started to migrate to new config

This commit is contained in:
Hellow
2023-09-10 16:27:09 +02:00
parent 80c4117629
commit d2dd015817
30 changed files with 798 additions and 634 deletions

View File

@@ -1,13 +1,36 @@
from .sections.logging import LOGGING_SECTION
from .sections.audio import AUDIO_SECTION
from .sections.connection import CONNECTION_SECTION
from .sections.misc import MISC_SECTION
from .sections.paths import PATHS_SECTION
from typing import Tuple
from .sections.paths import LOCATIONS
from .config import Config
from .config_files import (
main_config,
logging_config,
youtube_config,
)
from .settings import read_config, write_config, load, set_name_to_value
_sections: Tuple[Config, ...] = (
main_config.config,
logging_config.config,
youtube_config.config
)
def read_config():
for section in _sections:
section.read()
load()
# special cases
if main_settings['tor']:
main_settings['proxies'] = {
'http': f'socks5h://127.0.0.1:{main_settings["tor_port"]}',
'https': f'socks5h://127.0.0.1:{main_settings["tor_port"]}'
}
def write_config():
for section in _sections:
section.write()
def set_name_to_value():
pass
main_settings: main_config.SettingsStructure = main_config.config.loaded_settings
logging_settings: logging_config.SettingsStructure = logging_config.config.loaded_settings
youtube_settings: youtube_config.SettingsStructure = youtube_config.config.loaded_settings

View File

@@ -0,0 +1,127 @@
from typing import Union, Tuple, Dict, Iterable, List
from datetime import datetime
import logging
import os
from ..exception.config import SettingNotFound, SettingValueError
from ..path_manager import LOCATIONS
from .base_classes import Description, Attribute, Section, EmptyLine, COMMENT_PREFIX
from .sections.audio import AUDIO_SECTION
from .sections.logging import LOGGING_SECTION
from .sections.connection import CONNECTION_SECTION
from .sections.misc import MISC_SECTION
from .sections.paths import PATHS_SECTION
LOGGER = logging.getLogger("config")
class Config:
def __init__(self):
self.config_elements: Tuple[Union[Description, Attribute, Section], ...] = (
Description("IMPORTANT: If you modify this file, the changes for the actual setting, will be kept as is.\n"
"The changes you make to the comments, will be discarded, next time you run music-kraken. "
"Have fun!"),
Description(f"Latest reset: {datetime.now()}"),
Description("Those are all Settings for the audio codec.\n"
"If you, for some reason wanna fill your drive real quickly, I mean enjoy HIFI music,\n"
"feel free to tinker with the Bitrate or smth. :)"),
AUDIO_SECTION,
Description("Modify how Music-Kraken connects to the internet:"),
CONNECTION_SECTION,
Description("Modify all your paths, except your config file..."),
PATHS_SECTION,
Description("For all your Logging needs.\n"
"If you found a bug, and wan't to report it, please set the Logging level to 0,\n"
"reproduce the bug, and attach the logfile in the bugreport. ^w^"),
LOGGING_SECTION,
Description("If there are stupid settings, they are here."),
MISC_SECTION,
Description("🏳️‍⚧️🏳️‍⚧️ Protect trans youth. 🏳️‍⚧️🏳️‍⚧️\n"),
)
self._length = 0
self._section_list: List[Section] = []
self.name_section_map: Dict[str, Section] = dict()
for element in self.config_elements:
if not isinstance(element, Section):
continue
self._section_list.append(element)
for name in element.name_attribute_map:
if name in self.name_section_map:
raise ValueError(f"Two sections have the same name: "
f"{name}: "
f"{element.__class__.__name__} {self.name_section_map[name].__class__.__name__}")
self.name_section_map[name] = element
self._length += 1
def set_name_to_value(self, name: str, value: str, silent: bool = True):
"""
:raises SettingValueError, SettingNotFound:
:param name:
:param value:
:return:
"""
if name not in self.name_section_map:
if silent:
LOGGER.warning(f"The setting \"{name}\" is either deprecated, or doesn't exist.")
return
raise SettingNotFound(setting_name=name)
LOGGER.debug(f"setting: {name} value: {value}")
self.name_section_map[name].modify_setting(setting_name=name, new_value=value)
def __len__(self):
return self._length
@property
def config_string(self) -> str:
return "\n\n".join(str(element) for element in self.config_elements)
def _parse_conf_line(self, line: str, index: int):
"""
:raises SettingValueError, SettingNotFound:
:param line:
:param index:
:return:
"""
line = line.strip()
if line.startswith(COMMENT_PREFIX):
return
if line == "":
return
if "=" not in line:
"""
TODO
No value error but custom conf error
"""
raise ValueError(f"Couldn't find the '=' in line {index}.")
line_segments = line.split("=")
name = line_segments[0]
value = "=".join(line_segments[1:])
self.set_name_to_value(name, value)
def read_from_config_file(self, path: os.PathLike):
with open(path, "r", encoding=LOCATIONS.FILE_ENCODING) as conf_file:
for section in self._section_list:
section.reset_list_attribute()
for i, line in enumerate(conf_file):
self._parse_conf_line(line, i+1)
def write_to_config_file(self, path: os.PathLike):
with open(path, "w", encoding=LOCATIONS.FILE_ENCODING) as conf_file:
conf_file.write(self.config_string)
def __iter__(self) -> Iterable[Attribute]:
for section in self._section_list:
for name, attribute in section.name_attribute_map.items():
yield attribute

View File

@@ -1,60 +1,123 @@
import re
from typing import Optional, List, Union, Iterable, Callable
from dataclasses import dataclass
import logging
import toml
from copy import deepcopy
from ...exception.config import SettingValueError
from ..utils import comment
LOGGER = logging.getLogger("config")
COMMENT_PREFIX = "#"
def comment_string(uncommented: str) -> str:
unprocessed_lines = uncommented.split("\n")
processed_lines: List[str] = []
for line in unprocessed_lines:
line: str = line.strip()
if line.startswith(COMMENT_PREFIX) or line == "":
processed_lines.append(line)
continue
line = COMMENT_PREFIX + " " + line
processed_lines.append(line)
return "\n".join(processed_lines)
@dataclass
class Description:
def __init__(self, string: str) -> None:
self.string = string
description: str
@property
def config_string(self) -> str:
return comment(self.string)
def toml_string(self):
return comment_string(self.description)
class EmptyLine(Description):
def __init__(self):
self.description = ""
class Attribute:
pattern: str = r'^.*a$'
rule: str = "This is a default string, it has no rule."
string_value: str = ""
def __init__(self, name: str, description: str, pattern: str = None, rule: str = None) -> None:
if pattern is not None:
self.pattern = pattern
if rule is not None:
self.rule = rule
def __init__(
self,
name: str,
default_value: any,
description: Optional[str] = None,
):
self.name = name
self.description = description
def validate(self, input_string: str) -> bool:
return re.match(self.REGEX, input_string) is None
self.raw_data = {name: default_value}
self.value = default_value
self.description: Optional[str] = description
def unparse_simple_value(self, value: any) -> any:
return value
def parse_simple_value(self, value: any) -> any:
return value
def output_parse(self):
return self.string_value.strip()
def _recursive_parse_object(self, __object, callback: Callable):
if isinstance(__object, dict):
for key, value in __object.items():
__object[key] = self._recursive_parse_object(value, callback)
def input_parse(self, input_string: str) -> str:
match_result = re.match(self.pattern, input_string)
if match_result is None:
raise SettingValueError(
setting_name=self.name,
setting_value=input_string,
rule=self.rule
)
return __object
return match_result.string
if isinstance(__object, Union[list, tuple]):
for i, item in enumerate(__object):
__object[i] = self._recursive_parse_object(item, callback)
return __object
return callback(__object)
def load_toml(self, loaded_toml: dict, loaded_settings: dict) -> bool:
"""
returns true if succesfull
"""
if self.name not in loaded_toml:
LOGGER.warning(f"No setting by the name {self.name} found in the settings file.")
loaded_settings[self.name] = self.value
return
self.raw_data = loaded_toml[self.name]
_object = deepcopy(loaded_toml[self.name])
try:
self._recursive_parse_object(_object, self.parse_simple_value)
except SettingValueError as settings_error:
logging.warning(settings_error)
return False
self.value = _object
loaded_settings[self.name] = self.value
return True
@property
def value(self) -> str:
raise NotImplementedError()
@property
def config_string(self) -> str:
return NotImplementedError()
def toml_string(self) -> str:
string = ""
if self.description is not None:
string += comment(self.description) + "\n"
string += toml.dumps(self.raw_data)
return string
def __str__(self):
return f"{self.description}\n{self.name}={self.value}"
attr = Attribute(name="hello world", description="fuck you", value="defaulte")
attr.input_parse("fafda")
attr.input_parse("eeee")

View File

@@ -1,34 +0,0 @@
from typing import List
from .attribute import Attribute
from ..utils import comment
class ListAttribute(Attribute):
def __init__(self, name: str, description: str, value: List[str], pattern: str = None, rule: str = None) -> None:
super().__init__(name, description, pattern, rule)
self.string_value_list = []
self.set_to_list(value)
def set_to_list(self, input_value_list: List[str]):
self.string_value_list = []
for input_value in input_value_list:
self.string_value_list.append(input_value)
def append(self, input_value: str):
self.string_value_list.append(self.input_parse(input_value))
@property
def value(self) -> str:
return [self.output_parse(element) for element in self.string_value_list]
@property
def config_string(self) -> str:
NEWLINE = "\n"
return f"[{self.name}.start]" \
f"{comment(self.description)}\n" \
f"{NEWLINE.join(self.name+'='+v for v in self.string_value_list)}\n" \
f"{comment('RULE: ' + self.rule)}\n" \
f"[{self.name}.end]"

View File

@@ -1,18 +0,0 @@
from ..utils import comment
from .attribute import Attribute
class SingleAttribute(Attribute):
def __init__(self, name: str, description: str, value: str, pattern: str = None, rule: str = None) -> None:
super().__init__(name, description, pattern, rule)
self.string_value = self.input_parse(value)
@property
def value(self) -> str:
return self.output_parse(self.string_value)
@property
def config_string(self) -> str:
return f"{comment(self.description)}\n" \
f"{self.name}={self.value}\n" \
f"{comment('RULE: ' + self.rule)}" \

View File

@@ -0,0 +1,149 @@
from pathlib import Path
from typing import Optional, Dict, Set
from urllib.parse import urlparse, urlunparse
import logging
from .attribute import Attribute
from ...exception.config import SettingValueError
class UrlAttribute(Attribute):
def parse_simple_value(self, value: any) -> any:
return urlparse(value)
def unparse_simple_value(self, value: any) -> any:
return urlunparse((value.scheme, value.netloc, value.path, value.params, value.query, value.fragment))
class PathAttribute(Attribute):
def parse_simple_value(self, value: any) -> any:
return Path(value)
def unparse_simple_value(self, value: any) -> any:
return value.resolve()
class SelectAttribute(Attribute):
def __init__(self, name: str, default_value: any, options: tuple, description: Optional[str] = None, ignore_options_for_description = False):
self.options: tuple = options
new_description = ""
if description is not None:
new_description += description
new_description += "\n"
if not ignore_options_for_description:
new_description += f"{{{', '.join(self.options)}}}"
super().__init__(name, default_value, description)
def parse_simple_value(self, value: any) -> any:
if value in self.options:
return value
raise SettingValueError(
setting_name=self.name,
setting_value=value,
rule=f"has to be in the options: {{{', '.join(self.options)}}}."
)
def unparse_simple_value(self, value: any) -> any:
return value
class IntegerSelect(Attribute):
def __init__(self, name: str, default_value: any, options: Dict[int, str], description: Optional[str] = None, ignore_options_for_description = False):
self.options: Dict[str, int] = options
self.option_values: Set[int] = set(self.options.values())
new_description = ""
if description is not None:
new_description += description
description_lines = []
if description is not None:
description_lines.append(description)
description_lines.append("The values can be either an integer or one of the following values:")
for number, option in self.options.items():
description_lines.append(f"{number}: {option}")
super().__init__(name, default_value, "\n".join(description_lines))
def parse_simple_value(self, value: any) -> any:
if isinstance(value, str):
if value not in self.options:
raise SettingValueError(
setting_name=self.name,
setting_value=value,
rule=f"has to be in the options: {{{', '.join(self.options.keys())}}}, if it is a string."
)
return self.options[value]
return value
def unparse_simple_value(self, value: int) -> any:
if value in self.option_values:
for option, v in self.options.items():
if v == value:
return value
return value
ID3_2_FILE_FORMATS = frozenset((
"mp3", "mp2", "mp1", # MPEG-1 ID3.2
"wav", "wave", "rmi", # RIFF (including WAV) ID3.2
"aiff", "aif", "aifc", # AIFF ID3.2
"aac", "aacp", # Raw AAC ID3.2
"tta", # True Audio ID3.2
))
_sorted_id3_2_formats = sorted(ID3_2_FILE_FORMATS)
ID3_1_FILE_FORMATS = frozenset((
"ape", # Monkey's Audio ID3.1
"mpc", "mpp", "mp+", # MusePack ID3.1
"wv", # WavPack ID3.1
"ofr", "ofs" # OptimFrog ID3.1
))
_sorted_id3_1_formats = sorted(ID3_1_FILE_FORMATS)
class AudioFormatAttribute(Attribute):
def __init__(self, name: str, default_value: any, description: Optional[str] = None, ignore_options_for_description = False):
new_description = ""
if description is not None:
new_description += description
new_description += "\n"
new_description += f"ID3.2: {{{', '.join(ID3_2_FILE_FORMATS)}}}\n"
new_description += f"ID3.1: {{{', '.join(ID3_1_FILE_FORMATS)}}}"
super().__init__(name, default_value, description)
def parse_simple_value(self, value: any) -> any:
value = value.strip().lower()
if value in ID3_2_FILE_FORMATS:
return value
if value in ID3_1_FILE_FORMATS:
logging.debug(f"setting audio format to a format that only supports ID3.1: {v}")
return value
raise SettingValueError(
setting_name=self.name,
setting_value=value,
rule="has to be a valid audio format, supporting id3 metadata"
)
def unparse_simple_value(self, value: any) -> any:
return value
class LoggerAttribute(Attribute):
def parse_simple_value(self, value: str) -> logging.Logger:
return logging.getLogger(self.value)
def unparse_simple_value(self, value: logging.Logger) -> any:
return value.name

View File

@@ -1,234 +0,0 @@
import logging
from dataclasses import dataclass
from typing import Optional, List, Union, Dict
from ..exception.config import SettingNotFound, SettingValueError
LOGGER = logging.getLogger("config")
COMMENT_PREFIX = "#"
def comment_string(uncommented: str) -> str:
unprocessed_lines = uncommented.split("\n")
processed_lines: List[str] = []
for line in unprocessed_lines:
line: str = line.strip()
if line.startswith(COMMENT_PREFIX) or line == "":
processed_lines.append(line)
continue
line = COMMENT_PREFIX + " " + line
processed_lines.append(line)
return "\n".join(processed_lines)
@dataclass
class Attribute:
name: str
description: Optional[str]
value: Union[str, List[str]]
def validate(self, value: str):
"""
This function validates a new value without setting it.
:raise SettingValueError:
:param value:
:return:
"""
pass
def set_value(self, value: str):
"""
:raise SettingValueError: if the value is invalid for this setting
:param value:
:return:
"""
self.validate(value)
self.value = value
@property
def description_as_comment(self):
return comment_string(self.description)
@property
def object_from_value(self):
return self.value
def __str__(self):
return f"{self.description_as_comment}\n{self.name}={self.value}"
class SingleAttribute(Attribute):
value: str
class StringAttribute(SingleAttribute):
@property
def object_from_value(self) -> str:
return self.value.strip()
class IntAttribute(SingleAttribute):
def validate(self, value: str):
if not value.isdigit():
raise SettingValueError(
setting_name=self.name,
setting_value=value,
rule="has to be a digit (an int)"
)
@property
def object_from_value(self) -> int:
if self.value.isdigit():
return int(self.value)
class BoolAttribute(SingleAttribute):
def validate(self, value: str):
if value.lower().strip() not in {"true", "false"}:
raise SettingValueError(
setting_name=self.name,
setting_value=value,
rule="has to be a bool (true/false)"
)
@property
def object_from_value(self) -> bool:
return self.value.lower().strip() in {"yes", "y", "t", "true"}
class FloatAttribute(SingleAttribute):
def validate(self, value: str):
try:
float(value)
except ValueError:
raise SettingValueError(
setting_name=self.name,
setting_value=value,
rule="has to be numeric (an int or float)"
)
@property
def object_from_value(self) -> float:
return float(self.value)
class ListAttribute(Attribute):
value: List[str]
has_default_values: bool = True
def __len__(self):
return len(self.value)
def set_value(self, value: str):
"""
Due to lists being represented as multiple lines with the same key,
this appends, rather than setting anything.
:raise SettingValueError:
:param value:
:return:
"""
self.validate(value)
# resetting the list to an empty list, if this is the first config line to load
if self.has_default_values:
self.value = []
self.has_default_values = False
if value in self.value:
return
self.value.append(value)
def __str__(self):
return f"{self.description_as_comment}\n" + \
"\n".join(f"{self.name}={element}" for element in self.value)
def single_object_from_element(self, value: str):
return value
@property
def object_from_value(self) -> list:
"""
THIS IS NOT THE PROPERTY TO OVERRIDE WHEN INHERITING ListAttribute
single_object_from_element
:return:
"""
parsed = list()
for raw in self.value:
parsed.append(self.single_object_from_element(raw))
return parsed
@dataclass
class Description:
description: str
def __str__(self):
return comment_string(self.description)
class EmptyLine(Description):
def __init__(self):
self.description = ""
class Section:
"""
A placeholder class
"""
attribute_list: List[Union[
Attribute,
Description
]]
def __init__(self):
self.name_attribute_map: Dict[str, Attribute] = dict()
self.index_values()
def __str__(self):
return "\n".join(attribute.__str__() for attribute in self.attribute_list)
def index_values(self):
for element in self.attribute_list:
if not isinstance(element, Attribute):
continue
if element.name in self.name_attribute_map:
raise ValueError(f"Two different Attributes have the same name: "
f"{self.name_attribute_map[element.name]} {element}")
self.name_attribute_map[element.name] = element
def modify_setting(self, setting_name: str, new_value: str):
"""
:raise SettingValueError, SettingNotFound:
:param setting_name:
:param new_value:
:return:
"""
if setting_name not in self.name_attribute_map:
raise SettingNotFound(
setting_name=setting_name
)
self.name_attribute_map[setting_name].set_value(new_value)
def reset_list_attribute(self):
for attribute in self.attribute_list:
if not isinstance(attribute, ListAttribute):
continue
attribute.has_default_values = True

View File

@@ -1,127 +1,37 @@
from typing import Union, Tuple, Dict, Iterable, List
from datetime import datetime
from typing import Tuple, Union
from pathlib import Path
import logging
import os
from ..exception.config import SettingNotFound, SettingValueError
from ..path_manager import LOCATIONS
from .base_classes import Description, Attribute, Section, EmptyLine, COMMENT_PREFIX
from .sections.audio import AUDIO_SECTION
from .sections.logging import LOGGING_SECTION
from .sections.connection import CONNECTION_SECTION
from .sections.misc import MISC_SECTION
from .sections.paths import PATHS_SECTION
import toml
LOGGER = logging.getLogger("config")
from .attributes.attribute import Attribute, Description, EmptyLine
class Config:
def __init__(self):
self.config_elements: Tuple[Union[Description, Attribute, Section], ...] = (
Description("IMPORTANT: If you modify this file, the changes for the actual setting, will be kept as is.\n"
"The changes you make to the comments, will be discarded, next time you run music-kraken. "
"Have fun!"),
Description(f"Latest reset: {datetime.now()}"),
Description("Those are all Settings for the audio codec.\n"
"If you, for some reason wanna fill your drive real quickly, I mean enjoy HIFI music,\n"
"feel free to tinker with the Bitrate or smth. :)"),
AUDIO_SECTION,
Description("Modify how Music-Kraken connects to the internet:"),
CONNECTION_SECTION,
Description("Modify all your paths, except your config file..."),
PATHS_SECTION,
Description("For all your Logging needs.\n"
"If you found a bug, and wan't to report it, please set the Logging level to 0,\n"
"reproduce the bug, and attach the logfile in the bugreport. ^w^"),
LOGGING_SECTION,
Description("If there are stupid settings, they are here."),
MISC_SECTION,
Description("🏳️‍⚧️🏳️‍⚧️ Protect trans youth. 🏳️‍⚧️🏳️‍⚧️\n"),
)
def __init__(self, componet_list: Tuple[Union[Attribute, Description, EmptyLine]], config_file: Path) -> None:
self.config_file: Path = config_file
self._length = 0
self._section_list: List[Section] = []
self.name_section_map: Dict[str, Section] = dict()
for element in self.config_elements:
if not isinstance(element, Section):
continue
self._section_list.append(element)
for name in element.name_attribute_map:
if name in self.name_section_map:
raise ValueError(f"Two sections have the same name: "
f"{name}: "
f"{element.__class__.__name__} {self.name_section_map[name].__class__.__name__}")
self.name_section_map[name] = element
self._length += 1
def set_name_to_value(self, name: str, value: str, silent: bool = True):
"""
:raises SettingValueError, SettingNotFound:
:param name:
:param value:
:return:
"""
if name not in self.name_section_map:
if silent:
LOGGER.warning(f"The setting \"{name}\" is either deprecated, or doesn't exist.")
return
raise SettingNotFound(setting_name=name)
LOGGER.debug(f"setting: {name} value: {value}")
self.name_section_map[name].modify_setting(setting_name=name, new_value=value)
def __len__(self):
return self._length
self.component_list: Tuple[Union[Attribute, Description, EmptyLine]] = componet_list
self.loaded_settings: dict = {}
@property
def config_string(self) -> str:
return "\n\n".join(str(element) for element in self.config_elements)
def toml_string(self):
"\n\n".join(component.toml_string for component in self.component_list)
def _parse_conf_line(self, line: str, index: int):
"""
:raises SettingValueError, SettingNotFound:
:param line:
:param index:
:return:
"""
line = line.strip()
if line.startswith(COMMENT_PREFIX):
def write(self):
with self.config_file.open("w") as conf_file:
conf_file.write(self.toml_string)
def read(self):
if not self.config_file.is_file():
logging.info(f"Config file at '{self.config_file}' doesn't exist => generating")
self.write()
return
toml_data = {}
with self.config_file.open("r") as conf_file:
toml_data = toml.load(conf_file)
if line == "":
return
if "=" not in line:
"""
TODO
No value error but custom conf error
"""
raise ValueError(f"Couldn't find the '=' in line {index}.")
line_segments = line.split("=")
name = line_segments[0]
value = "=".join(line_segments[1:])
self.set_name_to_value(name, value)
def read_from_config_file(self, path: os.PathLike):
with open(path, "r", encoding=LOCATIONS.FILE_ENCODING) as conf_file:
for section in self._section_list:
section.reset_list_attribute()
for i, line in enumerate(conf_file):
self._parse_conf_line(line, i+1)
def write_to_config_file(self, path: os.PathLike):
with open(path, "w", encoding=LOCATIONS.FILE_ENCODING) as conf_file:
conf_file.write(self.config_string)
def __iter__(self) -> Iterable[Attribute]:
for section in self._section_list:
for name, attribute in section.name_attribute_map.items():
yield attribute
for component in self.component_list:
if isinstance(component, Attribute):
component.load_toml(toml_data, self.loaded_settings)

View File

@@ -0,0 +1,99 @@
from typing import TypedDict, List
from urllib.parse import ParseResult
from logging import Logger
from pathlib import Path
import logging
from ...path_manager import LOCATIONS
from ..config import Config
from ..attributes.attribute import Attribute, EmptyLine
from ..attributes.special_attributes import (
IntegerSelect,
LoggerAttribute
)
config = Config([
Attribute(name="logging_format", default_value="%(levelname)s:%(name)s:%(message)s", description="""Logging settings for the actual logging:
Reference for the logging formats: https://docs.python.org/3/library/logging.html#logrecord-attributes"""),
IntegerSelect(
name="log_level",
default_value=str(logging.INFO),
options={
"CRITICAL": 50,
"ERROR": 40,
"WARNING": 30,
"INFO": 20,
"DEBUG": 10,
"NOTSET": 0
}
),
LoggerAttribute(
name="download_logger",
description="The logger for downloading.",
default_value="download"
),
LoggerAttribute(
name="tagging_logger",
description="The logger for tagging id3 containers.",
default_value="tagging"
),
LoggerAttribute(
name="codex_logger",
description="The logger for streaming the audio into an uniform codex.",
default_value="codex"
),
LoggerAttribute(
name="object_logger",
description="The logger for creating Data-Objects.",
default_value="object"
),
LoggerAttribute(
name="database_logger",
description="The logger for Database operations.",
default_value="database"
),
LoggerAttribute(
name="musify_logger",
description="The logger for the musify scraper.",
default_value="musify"
),
LoggerAttribute(
name="youtube_logger",
description="The logger for the youtube scraper.",
default_value="youtube"
),
LoggerAttribute(
name="youtube_music_logger",
description="The logger for the youtube music scraper.\n(The scraper is seperate to the youtube scraper)",
default_value="youtube_music"
),
LoggerAttribute(
name="metal_archives_logger",
description="The logger for the metal archives scraper.",
default_value="metal_archives"
),
LoggerAttribute(
name="genius_logger",
description="The logger for the genius scraper",
default_value="genius"
),
], LOCATIONS.get_config_file("logging"))
class SettingsStructure(TypedDict):
# logging
logging_format: str
log_level: int
download_logger: Logger
tagging_logger: Logger
codex_logger: Logger
object_logger: Logger
database_logger: Logger
musify_logger: Logger
youtube_logger: Logger
youtube_music_logger: Logger
metal_archives_logger: Logger
genius_logger: Logger

View File

@@ -0,0 +1,142 @@
from typing import TypedDict, List
from datetime import datetime
from urllib.parse import ParseResult
from logging import Logger
from pathlib import Path
from ...path_manager import LOCATIONS
from ..config import Config
from ..attributes.attribute import Attribute, EmptyLine, Description
from ..attributes.special_attributes import (
SelectAttribute,
PathAttribute,
AudioFormatAttribute,
)
config = Config([
Description(f"""IMPORTANT: If you modify this file, the changes for the actual setting, will be kept as is.
The changes you make to the comments, will be discarded, next time you run music-kraken. Have fun!
Latest reset: {datetime.now()}
_________ __
\\_ ___ \\ __ __ _/ |_ ____
/ \\ \\/ | | \\\\ __\\_/ __ \\
\\ \\____| | / | | \\ ___/
\\______ /|____/ |__| \\___ >
\\/ \\/
"""),
Attribute(name="hasnt_yet_started", default_value=False, description="This will be set automatically, to look if it needs to run the scripts that run on start."),
Attribute(name="bitrate", default_value=125, description="Streams the audio with given bitrate [kB/s]. Can't stream with a higher Bitrate, than the audio source provides."),
AudioFormatAttribute(name="audio_format", default_value="mp3", description="""Music Kraken will stream the audio into this format.
You can use Audio formats which support ID3.2 and ID3.1,
but you will have cleaner Metadata using ID3.2."""),
Attribute(name="result_history", default_value=False, description="""If enabled, you can go back to the previous results.
The consequence is a higher meory consumption, because every result is saved."""),
Attribute(name="history_length", default_value=8, description="""You can choose how far back you can go in the result history.
The further you choose to be able to go back, the higher the memory usage.
'-1' removes the Limit entirely."""),
EmptyLine(),
Attribute(name="sort_by_date", default_value=True, description="If this is set to true, it will set the albumsort attribute such that,\nthe albums are sorted by date"),
Attribute(name="sort_album_by_type", default_value=True, description="""If this is set to true, it will set the albumsort attribute such that,
the albums are put into categories before being sorted.
This means for example, the Studio Albums and EP's are always in front of Singles, and Compilations are in the back."""),
Attribute(name="download_path", default_value="{genre}/{artist}/{album}", description="""There are multiple fields, you can use for the path and file name:
- genre
- label
- artist
- album
- song
- album_type
The folder music kraken should put the songs into."""),
Attribute(name="download_file", default_value="{song}.{audio_format}", description="The filename of the audio file."),
SelectAttribute(name="album_type_blacklist", default_value=[
"Compilation Album",
"Live Album",
"Mixtape"
], options=("Studio Album", "EP (Extended Play)", "Single", "Live Album", "Compilation Album", "Mixtape", "Demo", "Other"), description="""Music Kraken ignores all albums of those types.
Following album types exist in the programm:"""),
EmptyLine(),
Attribute(name="proxies", default_value=[], description="This is a dictionary."),
Attribute(name="tor", default_value=False, description="""Route ALL traffic through Tor.
If you use Tor, make sure the Tor browser is installed, and running.I can't guarantee maximum security though!"""),
Attribute(name="tor_port", default_value=9150, description="The port, tor is listening. If tor is already working, don't change it."),
Attribute(name="chunk_size", default_value=1024, description="Size of the chunks that are streamed.\nHere could be some room for improvement."),
Attribute(name="show_download_errors_threshold", default_value=0.3, description="""If the percentage of failed downloads goes over this threshold,
all the error messages are shown."""),
EmptyLine(),
PathAttribute(name="music_directory", default_value=LOCATIONS.MUSIC_DIRECTORY, description="The directory, all the music will be downloaded to."),
PathAttribute(name="temp_directory", default_value=LOCATIONS.TEMP_DIRECTORY, description="All temporary stuff is gonna be dumped in this directory."),
PathAttribute(name="log_file", default_value=LOCATIONS.get_log_file("download_logs.log")),
PathAttribute(name="ffmpeg_binary", default_value=LOCATIONS.FFMPEG_BIN, description="Set the path to the ffmpeg binary."),
Attribute(
name="not_a_genre_regex",
description="These regular expressions tell music-kraken, which sub-folders of the music-directory\n"
"it should ignore, and not count to genres",
default_value=[
r'^\.' # is hidden/starts with a "."
]
),
EmptyLine(),
Attribute(name="happy_messages", default_value=[
"Support the artist.",
"Star Me: https://github.com/HeIIow2/music-downloader",
"🏳️‍⚧️🏳️‍⚧️ Trans rights are human rights. 🏳️‍⚧️🏳️‍⚧️",
"🏳️‍⚧️🏳️‍⚧️ Trans women are women, trans men are men, and enbies are enbies. 🏳️‍⚧️🏳️‍⚧️",
"🏴‍☠️🏴‍☠️ Unite under one flag, fck borders. 🏴‍☠️🏴‍☠️",
"Join my Matrix Space: https://matrix.to/#/#music-kraken:matrix.org",
"BPJM does cencorship.",
"🏳️‍⚧️🏳️‍⚧️ Protect trans youth. 🏳️‍⚧️🏳️‍⚧️",
"Klassenkampf",
"Rise Proletarians!!"
], description="""Just some nice and wholesome messages.
If your mindset has traits of a [file corruption], you might not agree.
But anyways... Freedom of thought, so go ahead and change the messages."""),
Attribute(name="id_bits", default_value=64, description="I really dunno why I even made this a setting.. Modifying this is a REALLY dumb idea."),
Description("🏳️‍⚧️🏳️‍⚧️ Protect trans youth. 🏳️‍⚧️🏳️‍⚧️\n"),
], LOCATIONS.get_config_file("main"))
class SettingsStructure(TypedDict):
hasnt_yet_started: bool
result_history: bool
history_length: int
happy_messages: List[str]
modify_gc: bool
id_bits: int
# audio
bitrate: int
audio_format: str
sort_by_date: bool
sort_album_by_type: bool
download_path: str
download_file: str
album_type_blacklist: List[str]
# connection
proxies: List[dict[str, str]]
tor: bool
tor_port: int
chunk_size: int
show_download_errors_threshold: float
# paths
music_directory: Path
temp_directory: Path
log_file: Path
not_a_genre_regex: List[str]
ffmpeg_binary: Path

View File

@@ -0,0 +1,45 @@
from typing import TypedDict, List
from urllib.parse import ParseResult
from logging import Logger
from pathlib import Path
from ...path_manager import LOCATIONS
from ..config import Config
from ..attributes.attribute import Attribute
from ..attributes.special_attributes import SelectAttribute, PathAttribute, UrlAttribute
config = Config([
UrlAttribute(name="invidious_instance", default_value="https://yt.artemislena.eu", description="""This is an attribute, where you can define the invidious instances,
the youtube downloader should use.
Here is a list of active ones: https://docs.invidious.io/instances/
Instances that use cloudflare or have source code changes could cause issues.
Hidden instances (.onion) will only work, when setting 'tor=true'."""),
UrlAttribute(name="piped_instance", default_value="https://piped-api.privacy.com.de", description="""This is an attribute, where you can define the pioed instances,
the youtube downloader should use.
Here is a list of active ones: https://github.com/TeamPiped/Piped/wiki/Instances
Instances that use cloudflare or have source code changes could cause issues.
Hidden instances (.onion) will only work, when setting 'tor=true"""),
Attribute(name="sleep_after_youtube_403", default_value=30, description="The time to wait, after youtube returned 403 (in seconds)"),
Attribute(name="youtube_music_api_key", default_value="AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30", description="""This is the API key used by YouTube-Music internally.
Dw. if it is empty, Rachel will fetch it automatically for you <333
(she will also update outdated api keys/those that don't work)"""),
Attribute(name="youtube_music_clean_data", default_value=True, description="If set to true, it exclusively fetches artists/albums/songs, not things like user channels etc."),
UrlAttribute(name="youtube_url", default_value=[
"https://www.youtube.com/",
"https://www.youtu.be/"
], description="""This is used to detect, if an url is from youtube, or any alternativ frontend.
If any instance seems to be missing, run music kraken with the -f flag."""),
Attribute(name="use_sponsor_block", default_value=True, description="Use sponsor block to remove adds or simmilar from the youtube videos.")
], LOCATIONS.get_config_file("youtube"))
class SettingsStructure(TypedDict):
# youtube
invidious_instance: ParseResult
piped_instance: ParseResult
sleep_after_youtube_403: float
youtube_music_api_key: str
youtube_music_clean_data: bool
youtube_url: List[ParseResult]
use_sponsor_block: bool

View File

@@ -4,9 +4,6 @@ from urllib.parse import ParseResult
from logging import Logger
from pathlib import Path
from .sections.paths import LOCATIONS
from .config import Config
from .base_classes import Section, Attribute
class SettingsStructure(TypedDict):
@@ -62,34 +59,3 @@ class SettingsStructure(TypedDict):
log_file: Path
not_a_genre_regex: List[str]
ffmpeg_binary: Path
settings: SettingsStructure = {}
config = Config()
set_name_to_value = config.set_name_to_value
def read_config():
if not LOCATIONS.CONFIG_FILE.is_file():
write_config()
config.read_from_config_file(LOCATIONS.CONFIG_FILE)
def write_config():
config.write_to_config_file(LOCATIONS.CONFIG_FILE)
def load():
read_config()
for section in config.config_elements:
if not isinstance(section, Section):
continue
for attribute in section.attribute_list:
if not isinstance(attribute, Attribute):
continue
settings[attribute.name] = attribute.object_from_value