Started work on making adapter-defined config

This commit is contained in:
Sumner Evans
2020-06-06 00:27:49 -06:00
parent 1426268925
commit cb551d865b
23 changed files with 516 additions and 221 deletions

View File

@@ -1,17 +1,35 @@
import hashlib
import logging
import os
import pickle
from dataclasses import asdict, dataclass, field, fields
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import List, Optional
from typing import Dict, Optional, Type
import yaml
import dataclasses_json
from dataclasses_json import dataclass_json, DataClassJsonMixin
from sublime.adapters import ConfigurationStore
from sublime.ui.state import UIState
# JSON decoder and encoder translations
decoder_functions = {
Path: (lambda p: Path(p) if p else None),
}
encoder_functions = {
Path: (lambda p: str(p.resolve()) if p else None),
}
for type_, translation_function in decoder_functions.items():
dataclasses_json.cfg.global_config.decoders[type_] = translation_function
dataclasses_json.cfg.global_config.decoders[Optional[type_]] = translation_function
for type_, translation_function in encoder_functions.items():
dataclasses_json.cfg.global_config.encoders[type_] = translation_function
dataclasses_json.cfg.global_config.encoders[Optional[type_]] = translation_function
class ReplayGainType(Enum):
NO = 0
TRACK = 1
@@ -30,55 +48,33 @@ class ReplayGainType(Enum):
}[replay_gain_type.lower()]
@dataclass_json
@dataclass
class ServerConfiguration:
name: str = "Default"
server_address: str = "http://yourhost"
local_network_address: str = ""
local_network_ssid: str = ""
username: str = ""
password: str = ""
sync_enabled: bool = True
disable_cert_verify: bool = False
version: int = 0
class ProviderConfiguration:
id: str
name: str
ground_truth_adapter_type: Type
ground_truth_adapter_config: ConfigurationStore
caching_adapter_type: Optional[Type] = None
caching_adapter_config: Optional[ConfigurationStore] = None
def migrate(self):
self.version = 0
_strhash: Optional[str] = None
def strhash(self) -> str:
# TODO (#197): make this configurable by the adapters the combination of the
# hashes will be the hash dir
"""
Returns the MD5 hash of the server's name, server address, and
username. This should be used whenever it's necessary to uniquely
identify the server, rather than using the name (which is not
necessarily unique).
>>> sc = ServerConfiguration(
... name='foo',
... server_address='bar',
... username='baz',
... )
>>> sc.strhash()
'6df23dc03f9b54cc38a0fc1483df6e21'
"""
if not self._strhash:
server_info = self.name + self.server_address + self.username
self._strhash = hashlib.md5(server_info.encode("utf-8")).hexdigest()
return self._strhash
self.ground_truth_adapter_type.migrate_configuration(
self.ground_truth_adapter_config
)
if self.caching_adapter_type:
self.caching_adapter_type.migrate_configuration(self.caching_adapter_config)
@dataclass
class AppConfiguration:
version: int = 3
cache_location: str = ""
class AppConfiguration(DataClassJsonMixin):
version: int = 5
cache_location: Optional[Path] = None
filename: Optional[Path] = None
# Servers
servers: List[ServerConfiguration] = field(default_factory=list)
current_server_index: int = -1
# Providers
providers: Dict[str, ProviderConfiguration] = field(default_factory=dict)
current_provider_id: Optional[str] = None
# Global Settings
song_play_notification: bool = True
@@ -96,19 +92,16 @@ class AppConfiguration:
@staticmethod
def load_from_file(filename: Path) -> "AppConfiguration":
args = {}
config = AppConfiguration()
try:
if filename.exists():
with open(filename, "r") as f:
field_names = {f.name for f in fields(AppConfiguration)}
args = yaml.load(f, Loader=yaml.CLoader).items()
args = dict(filter(lambda kv: kv[0] in field_names, args))
config = AppConfiguration.from_json(f.read())
except Exception:
pass
config = AppConfiguration(**args)
config.filename = filename
config.save()
return config
def __post_init__(self):
@@ -116,82 +109,67 @@ class AppConfiguration:
if not self.cache_location:
path = Path(os.environ.get("XDG_DATA_HOME") or "~/.local/share")
path = path.expanduser().joinpath("sublime-music").resolve()
self.cache_location = path.as_posix()
# Deserialize the YAML into the ServerConfiguration object.
if len(self.servers) > 0 and type(self.servers[0]) != ServerConfiguration:
self.servers = [ServerConfiguration(**sc) for sc in self.servers]
self.cache_location = str(path)
self._state = None
self._current_server_hash = None
self._current_provider_id = None
self.migrate()
def migrate(self):
for server in self.servers:
server.migrate()
for _, provider in self.providers.items():
provider.migrate()
if self.version < 4:
self.allow_song_downloads = not self.always_stream
self.version = 4
self.version = 5
self.state.migrate()
@property
def server(self) -> Optional[ServerConfiguration]:
if 0 <= self.current_server_index < len(self.servers):
return self.servers[self.current_server_index]
return None
def provider(self) -> Optional[ProviderConfiguration]:
return self.providers.get(self._current_provider_id or "")
@property
def state(self) -> UIState:
server = self.server
if not server:
if not (provider := self.provider):
return UIState()
# If the server has changed, then retrieve the new server's state.
if self._current_server_hash != server.strhash():
# If the provider has changed, then retrieve the new provider's state.
if self._current_provider_id != provider.id:
self.load_state()
return self._state
def load_state(self):
self._state = UIState()
if not self.server:
if not (provider := self.provider):
return
self._current_server_hash = self.server.strhash()
if (
state_file_location := self.state_file_location
) and state_file_location.exists():
self._current_provider_id = provider.id
if (state_filename := self._state_file_location) and state_filename.exists():
try:
with open(state_file_location, "rb") as f:
with open(state_filename, "rb") as f:
self._state = pickle.load(f)
except Exception:
logging.warning(f"Couldn't load state from {state_file_location}")
logging.warning(f"Couldn't load state from {state_filename}")
# Just ignore any errors, it is only UI state.
self._state = UIState()
@property
def state_file_location(self) -> Optional[Path]:
if self.server is None:
def _state_file_location(self) -> Optional[Path]:
if not (provider := self.provider):
return None
server_hash = self.server.strhash()
state_file_location = Path(os.environ.get("XDG_DATA_HOME") or "~/.local/share")
return state_file_location.expanduser().joinpath(
"sublime-music", server_hash, "state.pickle"
state_filename = Path(os.environ.get("XDG_DATA_HOME") or "~/.local/share")
return state_filename.expanduser().joinpath(
"sublime-music", provider.id, "state.pickle"
)
def save(self):
# Save the config as YAML.
self.filename.parent.mkdir(parents=True, exist_ok=True)
with open(self.filename, "w+") as f:
f.write(yaml.dump(asdict(self)))
f.write(self.to_json(indent=2, sort_keys=True))
# Save the state for the current server.
if state_file_location := self.state_file_location:
state_file_location.parent.mkdir(parents=True, exist_ok=True)
with open(state_file_location, "wb+") as f:
# Save the state for the current provider.
if state_filename := self._state_file_location:
state_filename.parent.mkdir(parents=True, exist_ok=True)
with open(state_filename, "wb+") as f:
pickle.dump(self.state, f)