Files
sublime-music/sublime/config.py
2020-05-08 11:30:23 -06:00

188 lines
5.9 KiB
Python

import hashlib
import logging
import os
import pickle
from dataclasses import asdict, dataclass, field, fields
from enum import Enum
from pathlib import Path
from typing import List, Optional
try:
import keyring
has_keyring = True
except ImportError:
has_keyring = False
import yaml
from sublime.state_manager import ApplicationState
from sublime.ui.state import UIState
class ReplayGainType(Enum):
NO = 0
TRACK = 1
ALBUM = 2
def as_string(self) -> str:
return ["no", "track", "album"][self.value]
@staticmethod
def from_string(replay_gain_type: str) -> "ReplayGainType":
return {
"no": ReplayGainType.NO,
"disabled": ReplayGainType.NO,
"track": ReplayGainType.TRACK,
"album": ReplayGainType.ALBUM,
}[replay_gain_type.lower()]
@dataclass(unsafe_hash=True)
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
def migrate(self):
self.version = 0
def strhash(self) -> str:
# TODO: needs to change to something better
"""
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'
"""
server_info = self.name + self.server_address + self.username
return hashlib.md5(server_info.encode("utf-8")).hexdigest()
@dataclass
class AppConfiguration:
servers: List[ServerConfiguration] = field(default_factory=list)
current_server_index: int = -1
cache_location: str = ""
max_cache_size_mb: int = -1 # -1 means unlimited
always_stream: bool = False # always stream instead of downloading songs
download_on_stream: bool = True # also download when streaming a song
song_play_notification: bool = True
prefetch_amount: int = 3
concurrent_download_limit: int = 5
port_number: int = 8282
version: int = 3
serve_over_lan: bool = True
replay_gain: ReplayGainType = ReplayGainType.NO
filename: Optional[Path] = None
@staticmethod
def load_from_file(filename: Path) -> "AppConfiguration":
args = {}
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(**args)
config.filename = filename
return config
def __post_init__(self):
# Default the cache_location to ~/.local/share/sublime-music
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._state = None
self._current_server_hash = None
def migrate(self):
for server in self.servers:
server.migrate()
self.version = 3
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
@property
def state(self) -> UIState:
server = self.server
if not server:
return UIState()
# If the server has changed, then retrieve the new server's state.
# TODO: if things are slow, then use a different hash
if self._current_server_hash != server.strhash():
self.load_state()
return self._state
def load_state(self):
if not self.server:
return
self._current_server_hash = self.server.strhash()
if self.state_file_location.exists():
try:
with open(self.state_file_location, "rb") as f:
self._state = UIState(**pickle.load(f))
except Exception:
logging.warning(f"Couldn't load state from {self.state_file_location}")
# Just ignore any errors, it is only UI state.
self._state = UIState()
# Do the import in the function to avoid circular imports.
from sublime.cache_manager import CacheManager
from sublime.adapters import AdapterManager
CacheManager.reset(self)
AdapterManager.reset(self)
@property
def state_file_location(self) -> Path:
assert self.server is not 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"
)
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)))
# Save the state for the current server.
self.state_file_location.parent.mkdir(parents=True, exist_ok=True)
with open(self.state_file_location, "wb+") as f:
pickle.dump(asdict(self.state), f)