import itertools import logging from pathlib import Path from .error import VBANCMDError try: import tomllib except ModuleNotFoundError: import tomli as tomllib from .kinds import request_kind_map as kindmap logger = logging.getLogger(__name__) class TOMLStrBuilder: """builds a config profile, as a string, for the toml parser""" def __init__(self, kind): self.kind = kind self.higher = itertools.chain( [f'strip-{i}' for i in range(kind.num_strip)], [f'bus-{i}' for i in range(kind.num_bus)], ) def init_config(self, profile=None): self.virt_strip_params = ( [ 'mute = false', 'mono = false', 'solo = false', 'gain = 0.0', ] + [f'A{i} = false' for i in range(1, self.kind.phys_out + 1)] + [f'B{i} = false' for i in range(1, self.kind.virt_out + 1)] ) self.phys_strip_params = self.virt_strip_params + [ 'comp.knob = 0.0', 'gate.knob = 0.0', 'denoiser.knob = 0.0', 'eq.on = false', ] self.bus_float = ['gain = 0.0'] self.bus_params = [ 'mono = false', 'eq.on = false', 'mute = false', 'gain = 0.0', ] if profile == 'reset': self.reset_config() def reset_config(self): self.phys_strip_params = list( map(lambda x: x.replace('B1 = false', 'B1 = true'), self.phys_strip_params) ) self.virt_strip_params = list( map(lambda x: x.replace('A1 = false', 'A1 = true'), self.virt_strip_params) ) def build(self, profile='reset'): self.init_config(profile) toml_str = str() for eachclass in self.higher: toml_str += f'[{eachclass}]\n' toml_str = self.join(eachclass, toml_str) return toml_str def join(self, eachclass, toml_str): kls, index = eachclass.split('-') match kls: case 'strip': toml_str += ('\n').join( self.phys_strip_params if int(index) < self.kind.phys_in else self.virt_strip_params ) case 'bus': toml_str += ('\n').join(self.bus_params) case _: pass return toml_str + '\n' class TOMLDataExtractor: def __init__(self, file): with open(file, 'rb') as f: self._data = tomllib.load(f) @property def data(self): return self._data @data.setter def data(self, value): self._data = value def dataextraction_factory(file): """ factory function for parser this opens the possibility for other parsers to be added """ if file.suffix == '.toml': extractor = TOMLDataExtractor else: raise ValueError('Cannot extract data from {}'.format(file)) return extractor(file) class SingletonType(type): """ensure only a single instance of Loader object""" _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(SingletonType, cls).__call__(*args, **kwargs) return cls._instances[cls] class Loader(metaclass=SingletonType): """ invokes the parser checks if config already in memory loads data into memory if not found """ def __init__(self, kind): self._kind = kind self.logger = logger.getChild(self.__class__.__name__) self._configs = dict() self.defaults(kind) self.parser = None def defaults(self, kind): self.builder = TOMLStrBuilder(kind) toml_str = self.builder.build() self.register('reset', tomllib.loads(toml_str)) def parse(self, identifier, data): if identifier in self._configs: self.logger.info( f'config file with name {identifier} already in memory, skipping..' ) return try: self.parser = dataextraction_factory(data) except tomllib.TOMLDecodeError as e: ERR_MSG = (str(e), f'When attempting to load {identifier}.toml') self.logger.error(f'{type(e).__name__}: {" ".join(ERR_MSG)}') return return True def register(self, identifier, data=None): self._configs[identifier] = data if data else self.parser.data self.logger.info(f'config {self.name}/{identifier} loaded into memory') def deregister(self): self._configs.clear() self.defaults(self._kind) @property def configs(self): return self._configs @property def name(self): return self._kind.name def loader(kind): """ traverses defined paths for config files directs the loader returns configs loaded into memory """ logger_loader = logger.getChild('loader') loader = Loader(kind) for path in ( Path.cwd() / 'configs' / kind.name, Path.home() / '.config' / 'vban-cmd' / kind.name, Path.home() / 'Documents' / 'Voicemeeter' / 'configs' / kind.name, ): if path.is_dir(): logger_loader.info(f'Checking [{path}] for TOML config files:') for file in path.glob('*.toml'): identifier = file.with_suffix('').stem if loader.parse(identifier, file): loader.register(identifier) return loader.configs def request_config(kind_id: str): """ config entry point. Returns all configs loaded into memory for a kind """ try: configs = loader(kindmap(kind_id)) except KeyError: raise VBANCMDError(f'Unknown Voicemeeter kind {kind_id}') return configs