188 lines
5.9 KiB
Python
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)
|