""" Utilities for Grabber. """ import copy import logging import re import sys from traceback import format_exception from winreg import ConnectRegistry, OpenKey, QueryValueEx, HKEY_CURRENT_USER LOG_FILE = 'Grabber_error.log' log = logging.getLogger('Grabber') log.setLevel(logging.DEBUG) formatter = logging.Formatter('{name:<15}:{levelname:<7}:{lineno:4d}: {message}', style="{") filehandler = logging.FileHandler(LOG_FILE, encoding='utf-8') filehandler.setFormatter(formatter) filehandler.setLevel(logging.CRITICAL) log.addHandler(filehandler) def get_logger(string): """ Ensure logger is configured before use. """ return logging.getLogger(string) def except_hook(cls, exception, traceback): """ If frozen, catch error and exit. Dev, prints error but may continue if possible. """ critical_log = get_logger('Grabber') error = "".join(format_exception(cls, exception, traceback)) critical_log.critical(f'Encountered fatal error:\n\n{error}') # If imports fail below, the error, the error is still printed! try: if getattr(sys, 'frozen', False): warn_user(error) else: sys.__excepthook__(cls, exception, traceback) except: pass # Override PyQt exception handling sys.excepthook = except_hook from PyQt5.QtCore import QMimeData from PyQt5.QtGui import QFont, QColor, QGuiApplication, QClipboard from PyQt5.QtWidgets import QApplication, QMessageBox FONT_CONSOLAS = QFont() FONT_CONSOLAS.setFamily('Consolas') FONT_CONSOLAS.setPixelSize(13) def warn_user(error): app = QApplication.instance() if not app: app = QApplication(sys.argv) QMessageBox.warning(None, 'ERROR!', 'An critical error happened running the program. Please forward this error to developer:\n\n' f'{error}', QMessageBox.Ok) QApplication.exit(1) def to_clipboard(text): mime = QMimeData() mime.setText(text) board = QGuiApplication.clipboard() board.setMimeData(mime, mode=QClipboard.Clipboard) def path_shortener(full_path: str): """ Formats a path to a shorter version, for cleaner UI.""" # Convert to standard style, use / not \\ full_path = full_path.replace('\\', '/') if full_path[-1] != '/': full_path = full_path + '/' if len(full_path) > 20: times = 0 for integer, letter in enumerate(reversed(full_path)): if letter == '/': split = -integer - 1 times += 1 if times == 3 and full_path.count('/') >= 4: short_path = ''.join([full_path[:full_path.find('/') + 1], '...', full_path[split:]]) break elif times == 3: split = full_path.find('/', split) short_path = ''.join([full_path[:full_path.find('/') + 1], '...', full_path[split:]]) break else: short_path = full_path else: short_path = full_path return short_path def color_text(text: str, color: str = 'darkorange', weight: str = 'bold', sections: tuple = None) -> str: """ Formats a piece of string to be colored/bolded. Also supports having a section of the string colored. """ text = text.replace('\n', '<br>') if not sections: string = ''.join(['<span style=\"color:', color, '; font-weight:', weight, """;\" >""", text, "</span>"] ) else: work_text = text[sections[0]:sections[1]] string = ''.join([text[:sections[0]], '<span style=\"color:', color, '; font-weight:', weight, """;\" >""", work_text, "</span>", text[sections[1]:]] ) return string def format_in_list(command, option): com = re.compile(r'{.+\}') split_command = command.split() for index, item in enumerate(split_command): if '{}' in item and com.search(item) is None: split_command[index] = item.format(option) return split_command return split_command def get_win_accent_color(): """ Return the Windows 10 accent color used by the user in a HEX format Windows specific """ # Open the registry registry = ConnectRegistry(None, HKEY_CURRENT_USER) key = OpenKey(registry, r'Software\Microsoft\Windows\DWM') key_value = QueryValueEx(key, 'AccentColor') accent_int = key_value[0] accent_hex = hex(accent_int) # Remove FF offset and convert to HEX again accent_hex = str(accent_hex)[4:] # Remove prefix accent = accent_hex[4:6] + accent_hex[2:4] + accent_hex[0:2] return '#' + accent class LessNiceDict(dict): def values(self): return def items(self): return def __str__(self): return 'No printing' def __repr__(self): return 'No printing' class SettingsError(Exception): pass class ProfileLoadError(Exception): pass class SettingsClass: """Holds settings and handles manipulation""" def __init__(self, settings, profiles, filehandler=None): if not settings: raise SettingsError('Empty settings file!') if any((i in settings for i in ('Profiles', 'Other stuff', 'Settings'))): settings, old_profiles = self._upgrade_settings(settings) # Check for older settings files. if not profiles: profiles = old_profiles else: old_profiles.update(profiles) profiles = copy.deepcopy(old_profiles) try: self._userdefined = settings['default'] self._parameters = settings['parameters'] except KeyError as e: raise SettingsError(f'Missing section {e}') self._profiles = profiles self._filehandler = filehandler self.need_parameters = [] self._validate_settings() def __enter__(self): return self._parameters def __exit__(self, exc_type, exc_val, exc_tb): if self._filehandler is not None: self._filehandler.save_settings(self.settings_data) def __getitem__(self, item): return self._parameters[item] @property def user_options(self): return self._userdefined def get_favorites(self): return self._userdefined['favorites'] def is_activate(self, parameter): return self._parameters[parameter]['state'] def get_active_setting(self, parameter): param = self._parameters[parameter] if '{}' in param['command']: active = param['active option'] return param['options'][active] else: return '' @property def parameters(self) -> dict: return self._parameters @property def settings_data(self): return {'default': self._userdefined, 'parameters': self._parameters} @property def current_profile(self): return self._userdefined['current_profile'] @property def profiles(self): return list(self._profiles.keys()) @property def profiles_data(self): return self._profiles def create_profile(self, profile): self._profiles[profile] = copy.deepcopy(self._parameters) self._userdefined['current_profile'] = profile def delete_profile(self, profile): del self._profiles[profile] self._userdefined['current_profile'] = '' def remove_parameter_option(self, parameter, index): del self._parameters[parameter]['options'][index] option = self._parameters[parameter]['active option'] option -= 1 if option > 0 else 0 self._parameters[parameter]['active option'] = option if not option: self.need_parameters.append(parameter) # TODO: Remove options from profiles too. At least download options. Except when it's the selected option!!! def add_parameter_option(self, parameter, option): self._parameters[parameter]['options'].insert(0, option) def change_profile(self, profile): if self.current_profile == profile: return True if profile not in self._profiles: return False for param, data in self._profiles[profile].items(): if param not in self._parameters: self._parameters[param] = data continue if '{}' in data['command'] and self._parameters[param]['options'] != data['options']: self._parameters[param]['options'] = data['options'] + \ [i for i in self._parameters[param]['options'] if i not in data['options']] new_data = {k: v for k, v in data.items() if k != 'options'} self._parameters[param].update(new_data) else: self._parameters[param].update(data) self._userdefined['current_profile'] = profile return True @staticmethod def _upgrade_settings(old_settings): # print('Upgrading settings!!') settings = {} try: settings['default'] = copy.deepcopy(old_settings['Other stuff']) if old_settings['Favorites']: settings['default']['favorites'] = copy.deepcopy(old_settings['Favorites']) settings['parameters'] = copy.deepcopy(old_settings['Settings']) except KeyError: pass try: profiles = copy.deepcopy(old_settings['Profiles']) except KeyError: profiles = {} return settings, profiles def _validate_settings(self): # User defined part global base_settings missing_settings = {} # TODO: Validated that each settings is a dict before checking for keys. keys = base_settings['default'].keys() for key in keys: if key not in self._userdefined: self._userdefined[key] = get_base_setting('default', key) keys = base_settings['parameters'] for profile in self.profiles: for key in keys: if key not in self._profiles[profile]: self._profiles[profile][key] = get_base_setting('parameters', key) # Parameters keys = ['command', 'dependency', 'options', 'state', 'tooltip'] for setting in {i for i in base_settings['parameters'] if i not in self._parameters}: self._parameters[setting] = copy.deepcopy(base_settings['parameters'][setting]) for setting, option in self._parameters.items(): # setting: The name of the setting, like "Ignore errors" # option: The dict which contains the base keys. # key (Define below): is a key in the base settings for key in keys: # Check if all base keys are in the options. if key not in option.keys(): # Check if the current setting has already logged a missing key # If it hasn't, create an entry in the missing_settings dict, as a list. # If it's there, then add the key to the missing list. if setting not in missing_settings.keys(): missing_settings[setting] = [key] else: missing_settings[setting].append(key) # Check if the current setting is missing options for the command, when needed. # Disable the setting by default. Possibly alert the user. elif key == 'command': if '{}' in option[key]: if not option['options']: # print(f'{setting} currently lacks any valid options!') if 'state' in option.keys() and setting != 'Download location': self._parameters[setting]['state'] = False # Add to a list over options to add setting to. self.need_parameters.append(setting) if missing_settings: raise SettingsError('\n'.join(['Settings file is corrupt/missing:', '-' * 20, *[f'{key}:\n - {", ".join(value)}' if value else f"{key}" for key, value in missing_settings.items()], '-' * 20])) if not self._parameters['Download location']['options']: # Checks for a download setting, set the current path to that. path = self._filehandler.work_dir + '/DL/' self._parameters['Download location']['options'] = [path] try: # Checks if the active option is valid, if not reset to the first item. for setting in self._parameters: options = self._parameters[setting]['options'] if options is not None: # Check if active option is a valid number. if not (0 <= self._parameters[setting]['active option'] < len(options)): self._parameters[setting]['active option'] = 0 # Catch if the setting is missing for needed options. except KeyError as error: raise SettingsError(f'{setting} is missing a needed option {error}.') # Catches multiple type errors. except TypeError as error: raise SettingsError(f'An unexpected type was encountered for setting:\n - {setting}\n -- {error}') self._filehandler.save_profiles(self.profiles_data) self._filehandler.save_settings(self.settings_data) stylesheet = """ QWidget {{ background-color: {background_light}; color: {text_normal}; }} QMainWindow {{ background-color: {background_dark}; color: red; }} QMenu::separator {{ height: 2px; }} QFrame#line {{ color: {background_dark}; }} QLabel {{ background: #484848; padding: 2px; border-radius: 2px; outline: 0; }} QTabWidget::pane {{ border: none; }} QMenu::item {{ border: none; padding: 3px 20px 3px 5px }} QMenu {{ border: 1px solid {background_dark}; }} QMenu::item:selected {{ background-color: {background_dark}; }} QMenu::item:disabled {{ color: #808080; }} QTabWidget {{ background-color: {background_dark}; }} QTabBar {{ color: {background_dark}; background: {background_dark}; }} QTabBar::tab {{ color: {text_shaded}; background-color: {background_lightest}; border-bottom: none; border-left: 1px solid #00000000; min-width: 15ex; min-height: 7ex; }} QTabBar::tab:selected {{ color: white; background-color: {background_light}; }} QTabBar::tab:!selected {{ margin-top: 6px; background-color: {background_lightest} }} QTabWidget::tab-bar {{ border-top: 1px solid {background_dark}; }} QLineEdit {{ background-color: {background_dark}; color: {text_shaded}; border-radius: 0px; padding: 0 3px; }} QLineEdit:disabled {{ background-color: {background_dark}; color: #505050; border-radius: none; }} QTextEdit {{ background-color: {background_light}; color: {text_shaded}; border: red solid 1px; }} QTextEdit#TextFileEdit {{ background-color: {background_dark}; color: {text_shaded}; border: red solid 1px; border-radius: 2px; }} QListWidget {{ outline: none; outline-width: 0px; background: {background_dark}; border: 1px solid {background_dark}; border-radius: 2px; }} QScrollBar::vertical {{ border: none; background-color: transparent; width: 10px; margin: 0px 0px 0px 0px; }} QScrollBar::vertical#main {{ background-color: {background_dark}; }} QScrollBar::sub-line:vertical, QScrollBar::add-line:vertical {{ border: none; background: none; width: 0px; height: 0px; }} QScrollBar::handle:vertical {{ background: {background_dark}; min-height: 20px; border-radius: 5px; }} QScrollBar::handle:vertical#main {{ background: {background_darkest}; }} QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ background: none; }} QPushButton {{ background-color: {background_dark}; color: white; border: 1px solid transparent; border-radius: none; width: 60px; height: 20px; }} QPushButton:disabled {{ border: 1px solid {background_dark}; background-color: transparent; color: #757575; }} QPushButton:pressed {{ background-color: #101010; color: white; }} QTreeWidget {{ selection-color: red; border: none; outline: none; outline-width: 0px; selection-background-color: blue; }} QTreeWidget::item {{ height: 16px; }} QTreeWidget::item:disabled {{ color: grey; }} QTreeWidget::item:hover, QTreeWidget::item:selected {{ background-color: transparent; color: white; }} QComboBox {{ border: 1px solid {background_dark}; border-radius: 0px; background-color: {background_dark}; color: {text_shaded}; padding-right: 5px; padding-left: 5px; }} QComboBox::drop-down {{ border: 0px; background: none; }} QComboBox::disabled {{ color: {background_light}; }} """ base_settings = dict() base_settings['profiles'] = {} base_settings['parameters'] = {} base_settings['default'] = { 'multidl_txt': '', "parallel": False, 'current_profile': '', 'select_on_focus': True, 'favorites': [], 'show_collapse_arrows': False, 'use_win_accent': False, 'custom': { "command": "Custom", "state": False, "tooltip": "Custom option, double click to edit." } } base_settings['parameters']['Convert to audio'] = { "active option": 0, "command": "-x --audio-format {}", "dependency": None, "options": ['mp3'], "state": False, "tooltip": "Convert video files to audio-only files\n" "Requires ffmpeg, avconv and ffprobe or avprobe." } base_settings['parameters']["Add thumbnail"] = { "active option": 0, "command": "--embed-thumbnail", "dependency": 'Convert to audio', "options": None, "state": False, "tooltip": "Include thumbnail on audio files." } base_settings['parameters']['Audio quality'] = { "active option": 0, "command": "--audio-quality {}", "dependency": 'Convert to audio', "options": ['0', '5', '9'], "state": False, "tooltip": "Specify ffmpeg/avconv audio quality.\ninsert" "a value between\n0 (better) and 9 (worse)" "for VBR\nor a specific bitrate like 128K" } base_settings['parameters']['Ignore errors'] = { "active option": 0, "command": "-i", "dependency": None, "options": None, "state": False, "tooltip": "Ignores errors, and jumps to next element instead of stopping." } base_settings['parameters']['Download location'] = { "active option": 0, "command": "-o {}", "dependency": None, "options": None, "state": False, "tooltip": "Select download location." } base_settings['parameters']['Strict file names'] = { "active option": 0, "command": "--restrict-filenames", "dependency": None, "options": None, "state": False, "tooltip": "Sets strict naming, to prevent unsupported characters in names." } base_settings['parameters']['Keep archive'] = { "active option": 0, "command": "--download-archive {}", "dependency": None, "options": ['Archive.txt'], "state": False, "tooltip": "Saves links to a textfile to avoid duplicate downloads later." } base_settings['parameters']['Force generic extractor'] = { "active option": 0, "command": "--force-generic-extractor", "dependency": None, "options": None, "state": False, "tooltip": "Force extraction to use the generic extractor" } base_settings['parameters']['Use proxy'] = { "active option": 0, "command": "--proxy {}", "dependency": None, "options": [], "state": False, "tooltip": "Use the specified HTTP/HTTPS/SOCKS proxy." } base_settings['parameters']['Socket timeout'] = { "active option": 0, "command": "--socket-timeout {}", "dependency": None, "options": [10, 60, 300], "state": False, "tooltip": "Time to wait before giving up, in seconds." } base_settings['parameters']['Source IP'] = { "active option": 0, "command": "--source-address {}", "dependency": None, "options": [], "state": False, "tooltip": "Client-side IP address to bind to." } base_settings['parameters']['Force ipv4/6'] = { "active option": 0, "command": "--{}", "dependency": None, "options": ['force-ipv4', 'force-ipv6'], "state": False, "tooltip": "Make all connections via ipv4/6." } base_settings['parameters']['Geo bypass URL'] = { "active option": 0, "command": "--geo-verification-proxy {}", "dependency": None, "options": [], "state": False, "tooltip": "Use this proxy to verify the IP address for some geo-restricted sites.\n" "The default proxy specified by" " --proxy (or none, if the options is not present)\nis used for the actual downloading." } base_settings['parameters']['Geo bypass country CODE'] = { "active option": 0, "command": "--geo-bypass-country {}", "dependency": None, "options": [], "state": False, "tooltip": "Force bypass geographic restriction with explicitly provided\n" "two-letter ISO 3166-2 country code (experimental)." } base_settings['parameters']['Playlist start'] = { "active option": 0, "command": "--playlist-start {}", "dependency": None, "options": [], "state": False, "tooltip": "Playlist video to start at (default is 1)." } base_settings['parameters']['Playlist end'] = { "active option": 0, "command": "--playlist-end {}", "dependency": None, "options": [], "state": False, "tooltip": "Playlist video to end at (default is last)." } base_settings['parameters']['Playlist items'] = { "active option": 0, "command": "--playlist-items {}", "dependency": None, "options": [], "state": False, "tooltip": "Playlist video items to download.\n" "Specify indices of the videos in the playlist " "separated by commas like:\n\"1,2,5,8\" if you want to download videos " "indexed 1, 2, 5, 8 in the playlist.\nYou can specify range:" "\"1-3,7,10-13\"\nwill download the videos at index:\n1, 2, 3, 7, 10, 11, 12 and 13." } base_settings['parameters']['Match titles'] = { "active option": 0, "command": "--match-title {}", "dependency": None, "options": [], "state": False, "tooltip": "Download only matching titles (regex or caseless sub-string)." } base_settings['parameters']['Reject titles'] = { "active option": 0, "command": "--reject-title {}", "dependency": None, "options": [], "state": False, "tooltip": "Skip download for matching titles (regex or caseless sub-string)." } base_settings['parameters']['Max downloads'] = { "active option": 0, "command": "--max-downloads {}", "dependency": None, "options": [], "state": False, "tooltip": "Abort after downloading a certain number of files." } base_settings['parameters']['Minimum size'] = { "active option": 0, "command": "--min-filesize {}", "dependency": None, "options": [], "state": False, "tooltip": "Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)." } base_settings['parameters']['Maximum size'] = { "active option": 0, "command": "--max-filesize {}", "dependency": None, "options": [], "state": False, "tooltip": "Do not download any videos bigger than SIZE (e.g. 50k or 44.6m)." } base_settings['parameters']['No playlist'] = { "active option": 0, "command": "--no-playlist ", "dependency": None, "options": None, "state": False, "tooltip": "Download only the video, if the URL refers to a video and a playlist." } base_settings['parameters']['Download speed limit'] = { "active option": 0, "command": "--limit-rate {}", "dependency": None, "options": [], "state": False, "tooltip": "Maximum download rate in bytes per second (e.g. 50K or 4.2M)." } base_settings['parameters']['Retry rate'] = { "active option": 0, "command": "--retries {}", "dependency": None, "options": [10, 15], "state": False, "tooltip": "Number of retries (default is 10), or \"infinite\"." } base_settings['parameters']['Download order'] = { "active option": 0, "command": "--playlist-{}", "dependency": None, "options": ['reverse', 'random'], "state": False, "tooltip": "Download playlist videos in reverse/random order." } base_settings['parameters']['Prefer native/ffmpeg'] = { "active option": 0, "command": "--hls-prefer-{}", "dependency": None, "options": ['ffmpeg', 'native'], "state": False, "tooltip": "Use the native HLS downloader instead of ffmpeg, or vice versa." } base_settings['parameters']['Don\'t overwrite files'] = { "active option": 0, "command": "--no-overwrites", "dependency": None, "options": None, "state": False, "tooltip": "Do not overwrite files" } base_settings['parameters']['Don\'t continue files'] = { "active option": 0, "command": "--no-continue", "dependency": None, "options": None, "state": False, "tooltip": "Do not resume partially downloaded files." } base_settings['parameters']['Don\'t use .part files'] = { "active option": 0, "command": "--no-part", "dependency": None, "options": None, "state": False, "tooltip": "Do not use .part files - write directly into output file." } base_settings['parameters']['Verbose'] = { "active option": 0, "command": "--verbose", "dependency": None, "options": None, "state": False, "tooltip": "Print various debugging information." } base_settings['parameters']['Custom user agent'] = { "active option": 0, "command": "--user-agent {}", "dependency": None, "options": [], "state": False, "tooltip": "Specify a custom user agent." } base_settings['parameters']['Custom referer'] = { "active option": 0, "command": "--referer {}", "dependency": None, "options": [], "state": False, "tooltip": "Specify a custom referer, use if the video access is restricted to one domain." } base_settings['parameters']['Min sleep interval'] = { "active option": 0, "command": "--sleep-interval {}", "dependency": None, "options": [], "state": False, "tooltip": "Number of seconds to sleep before each download;\nwhen used " "alone or a lower bound of a range for randomized sleep before each\n" "download when used along with max sleep interval." } base_settings['parameters']['Max sleep interval'] = { "active option": 0, "command": "--max-sleep-interval {}", "dependency": "Min sleep interval", "options": [], "state": False, "tooltip": "Upper bound of a range for randomized sleep before each download\n" "(maximum possible number of seconds to sleep).\n" "Must only be used along with --min-sleep-interval." } base_settings['parameters']['Video format'] = { "active option": 0, "command": "--format {}", "dependency": None, "options": [], "state": False, "tooltip": "Video format code." } base_settings['parameters']['Write subtitle file'] = { "active option": 0, "command": "--write-sub", "dependency": None, "options": None, "state": False, "tooltip": "Write subtitle file." } base_settings['parameters']['Recode video'] = { "active option": 0, "command": "--recode-video {}", "dependency": None, "options": ['mp4', 'flv', 'ogg', 'webm', 'mkv', 'avi'], "state": False, "tooltip": "Encode the video to another format if necessary.\n" "Currently supported: mp4|flv|ogg|webm|mkv|avi." } base_settings['parameters']['No post overwrite'] = { "active option": 0, "command": "--no-post-overwrites", "dependency": None, "options": None, "state": False, "tooltip": "Do not overwrite post-processed files;\n" "the post-processed files are overwritten by default." } base_settings['parameters']['Embed subs'] = { "active option": 0, "command": "--embed-subs", "dependency": None, "options": None, "state": False, "tooltip": "Embed subtitles in the video (only for mp4, webm and mkv videos)" } base_settings['parameters']['Add metadata'] = { "active option": 0, "command": "--add-metadata", "dependency": None, "options": None, "state": False, "tooltip": "Write metadata to the video file." } base_settings['parameters']['Metadata from title'] = { "active option": 0, "command": "--metadata-from-title {}", "dependency": None, "options": [], "state": False, "tooltip": "Parse additional metadata like song title /" "artist from the video title.\nThe format" "syntax is the same as --output.\nRegular " "expression with named capture groups may" "also be used.\nThe parsed parameters replace " "existing values.\n\n" "Example:\n\"%(artist)s - %(title)s\" matches a" "title like \"Coldplay - Paradise\".\nExample" "(regex):\n\"(?P<artist>.+?) - (?P<title>.+)\"" } base_settings['parameters']['Merge output format'] = { "active option": 0, "command": "--merge-output-format {}", "dependency": None, "options": ["mp4", "mkv", "ogg", "webm", "flv"], "state": False, "tooltip": "If a merge is required (e.g. bestvideo+bestaudio)," "\noutput to given container format." "\nOne of mkv, mp4, ogg, webm, flv." "\nIgnored if no merge is required" } base_settings['parameters']['Username'] = { "active option": 0, "command": "--username {}", "dependency": None, "options": [], "state": False, "tooltip": "Username of your account. Password will be asked for on run." } base_settings['parameters']['Best video quality'] = { "active option": 0, "command": "--format ((571/272/402/337/315/313/401/336/308/400/271/335/303/299/399/137/248/334/302/298/398/247/136/333/244/135/397/332/243/134/396/331/242/133/395/330/160/394/278)[protocol!=http_dash_segments])+(bestaudio[acodec=opus]/bestaudio[protocol!=http_dash_segments])/best", "dependency": None, "options": None, "state": False, "tooltip": "This is a special command that uses --format" "\nThis picks best possible video quality (at the time of addition)" "\nDo not use when converting to audio!" } def get_base_settings() -> dict: settings = copy.deepcopy(base_settings) return settings def get_base_setting(section, setting): return copy.deepcopy(base_settings[section][setting]) def darken(color: QColor): return color.darker(150) def lighten(color: QColor): return color.lighter(150) surface = QColor('#484848') text = QColor('white') default_style = {'background_light': surface, 'background_dark': darken(surface), 'background_darkest': darken(darken(surface)), 'background_lightest': lighten(surface), 'text_shaded': darken(text), 'text_normal': text} def qcolorToStr(color_map: dict): return {k: v.name(QColor.HexRgb) for k, v in color_map.items()} def get_stylesheet(**kwargs): global default_style styles = default_style.copy() styles.update(kwargs) return stylesheet.format(**qcolorToStr(styles)) if __name__ == '__main__': pass # print(color_text('rests valued', sections=(2, 5))) # Testing of settings class: # import json # with open('..\\settings.json') as f: # profile = json.load(f) # s = SettingsClass(base_settings, profile['Profiles']) # s.change_profile('Music') # s.change_profile('Music') # s.change_profile('Video')