reverted config changes
This commit is contained in:
25
src/music_kraken/utils/config/__init__.py
Normal file
25
src/music_kraken/utils/config/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
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 .sections.paths import LOCATIONS
|
||||
from .config import Config
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
set_name_to_value = config.set_name_to_value
|
||||
|
||||
read_config()
|
60
src/music_kraken/utils/config/attributes/attribute.py
Normal file
60
src/music_kraken/utils/config/attributes/attribute.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import re
|
||||
|
||||
from ...exception.config import SettingValueError
|
||||
from ..utils import comment
|
||||
|
||||
|
||||
class Description:
|
||||
def __init__(self, string: str) -> None:
|
||||
self.string = string
|
||||
|
||||
@property
|
||||
def config_string(self) -> str:
|
||||
return comment(self.string)
|
||||
|
||||
|
||||
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
|
||||
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
def validate(self, input_string: str) -> bool:
|
||||
return re.match(self.REGEX, input_string) is None
|
||||
|
||||
def output_parse(self):
|
||||
return self.string_value.strip()
|
||||
|
||||
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 match_result.string
|
||||
|
||||
@property
|
||||
def value(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def config_string(self) -> str:
|
||||
return NotImplementedError()
|
||||
|
||||
|
||||
|
||||
attr = Attribute(name="hello world", description="fuck you", value="defaulte")
|
||||
attr.input_parse("fafda")
|
||||
attr.input_parse("eeee")
|
34
src/music_kraken/utils/config/attributes/list_attributes.py
Normal file
34
src/music_kraken/utils/config/attributes/list_attributes.py
Normal file
@@ -0,0 +1,34 @@
|
||||
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]"
|
@@ -0,0 +1,18 @@
|
||||
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)}" \
|
234
src/music_kraken/utils/config/base_classes.py
Normal file
234
src/music_kraken/utils/config/base_classes.py
Normal file
@@ -0,0 +1,234 @@
|
||||
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
|
127
src/music_kraken/utils/config/config.py
Normal file
127
src/music_kraken/utils/config/config.py
Normal 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
|
0
src/music_kraken/utils/config/sections/__init__.py
Normal file
0
src/music_kraken/utils/config/sections/__init__.py
Normal file
154
src/music_kraken/utils/config/sections/audio.py
Normal file
154
src/music_kraken/utils/config/sections/audio.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import logging
|
||||
|
||||
from ..base_classes import (
|
||||
SingleAttribute,
|
||||
FloatAttribute,
|
||||
StringAttribute,
|
||||
Section,
|
||||
Description,
|
||||
EmptyLine,
|
||||
BoolAttribute,
|
||||
ListAttribute
|
||||
)
|
||||
from ...enums.album import AlbumType
|
||||
from ...exception.config import SettingValueError
|
||||
|
||||
# Only the formats with id3 metadata can be used
|
||||
# https://www.audioranger.com/audio-formats.php
|
||||
# https://web.archive.org/web/20230322234434/https://www.audioranger.com/audio-formats.php
|
||||
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(SingleAttribute):
|
||||
def validate(self, value: str):
|
||||
v = self.value.strip().lower()
|
||||
if v not in ID3_1_FILE_FORMATS and v not in ID3_2_FILE_FORMATS:
|
||||
raise SettingValueError(
|
||||
setting_name=self.name,
|
||||
setting_value=value,
|
||||
rule="has to be a valid audio format, supporting id3 metadata"
|
||||
)
|
||||
|
||||
@property
|
||||
def object_from_value(self) -> str:
|
||||
v = self.value.strip().lower()
|
||||
if v in ID3_2_FILE_FORMATS:
|
||||
return v
|
||||
if v in ID3_1_FILE_FORMATS:
|
||||
logging.debug(f"setting audio format to a format that only supports ID3.1: {v}")
|
||||
return v
|
||||
|
||||
raise ValueError(f"Invalid Audio Format: {v}")
|
||||
|
||||
|
||||
class AlbumTypeListAttribute(ListAttribute):
|
||||
def validate(self, value: str):
|
||||
try:
|
||||
AlbumType(value.strip())
|
||||
except ValueError:
|
||||
raise SettingValueError(
|
||||
setting_name=self.name,
|
||||
setting_value=value,
|
||||
rule="has to be an existing album type"
|
||||
)
|
||||
|
||||
def single_object_from_element(self, value: str) -> AlbumType:
|
||||
return AlbumType(value)
|
||||
|
||||
|
||||
class AudioSection(Section):
|
||||
def __init__(self):
|
||||
self.BITRATE = FloatAttribute(
|
||||
name="bitrate",
|
||||
description="Streams the audio with given bitrate [kB/s]. "
|
||||
"Can't stream with a higher Bitrate, than the audio source provides.",
|
||||
value="125"
|
||||
)
|
||||
|
||||
self.AUDIO_FORMAT = AudioFormatAttribute(name="audio_format", value="mp3", description=f"""
|
||||
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.
|
||||
ID3.2: {', '.join(_sorted_id3_2_formats)}
|
||||
ID3.1: {', '.join(_sorted_id3_1_formats)}
|
||||
""".strip())
|
||||
|
||||
self.SORT_BY_DATE = BoolAttribute(
|
||||
name="sort_by_date",
|
||||
description="If this is set to true, it will set the albumsort attribute such that,\n"
|
||||
"the albums are sorted by date.",
|
||||
value="true"
|
||||
)
|
||||
|
||||
self.SORT_BY_ALBUM_TYPE = BoolAttribute(
|
||||
name="sort_album_by_type",
|
||||
description="If this is set to true, it will set the albumsort attribute such that,\n"
|
||||
"the albums are put into categories before being sorted.\n"
|
||||
"This means for example, the Studio Albums and EP's are always in front of Singles, "
|
||||
"and Compilations are in the back.",
|
||||
value="true"
|
||||
)
|
||||
|
||||
self.DOWNLOAD_PATH = StringAttribute(
|
||||
name="download_path",
|
||||
value="{genre}/{artist}/{album}",
|
||||
description="The folder music kraken should put the songs into."
|
||||
)
|
||||
|
||||
self.DOWNLOAD_FILE = StringAttribute(
|
||||
name="download_file",
|
||||
value="{song}.{audio_format}",
|
||||
description="The filename of the audio file."
|
||||
)
|
||||
|
||||
|
||||
self.ALBUM_TYPE_BLACKLIST = AlbumTypeListAttribute(
|
||||
name="album_type_blacklist",
|
||||
description="Music Kraken ignores all albums of those types.\n"
|
||||
"Following album types exist in the programm:\n"
|
||||
f"{', '.join(album.value for album in AlbumType)}",
|
||||
value=[
|
||||
AlbumType.COMPILATION_ALBUM.value,
|
||||
AlbumType.LIVE_ALBUM.value,
|
||||
AlbumType.MIXTAPE.value
|
||||
]
|
||||
)
|
||||
|
||||
self.attribute_list = [
|
||||
self.BITRATE,
|
||||
self.AUDIO_FORMAT,
|
||||
EmptyLine(),
|
||||
self.SORT_BY_DATE,
|
||||
self.SORT_BY_ALBUM_TYPE,
|
||||
Description("""
|
||||
There are multiple fields, you can use for the path and file name:
|
||||
- genre
|
||||
- label
|
||||
- artist
|
||||
- album
|
||||
- song
|
||||
- album_type
|
||||
""".strip()),
|
||||
self.DOWNLOAD_PATH,
|
||||
self.DOWNLOAD_FILE,
|
||||
self.ALBUM_TYPE_BLACKLIST,
|
||||
]
|
||||
super().__init__()
|
||||
|
||||
|
||||
AUDIO_SECTION = AudioSection()
|
157
src/music_kraken/utils/config/sections/connection.py
Normal file
157
src/music_kraken/utils/config/sections/connection.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from urllib.parse import urlparse, ParseResult
|
||||
import re
|
||||
|
||||
from ..base_classes import Section, FloatAttribute, IntAttribute, BoolAttribute, ListAttribute, StringAttribute
|
||||
from ...regex import URL_PATTERN
|
||||
from ...exception.config import SettingValueError
|
||||
|
||||
|
||||
class ProxAttribute(ListAttribute):
|
||||
def single_object_from_element(self, value) -> dict:
|
||||
return {
|
||||
'http': value,
|
||||
'https': value,
|
||||
'ftp': value
|
||||
}
|
||||
|
||||
|
||||
class UrlStringAttribute(StringAttribute):
|
||||
def validate(self, value: str):
|
||||
v = value.strip()
|
||||
url = re.match(URL_PATTERN, v)
|
||||
if url is None:
|
||||
raise SettingValueError(
|
||||
setting_name=self.name,
|
||||
setting_value=v,
|
||||
rule="has to be a valid url"
|
||||
)
|
||||
|
||||
@property
|
||||
def object_from_value(self) -> ParseResult:
|
||||
return urlparse(self.value)
|
||||
|
||||
|
||||
class UrlListAttribute(ListAttribute):
|
||||
def validate(self, value: str):
|
||||
v = value.strip()
|
||||
url = re.match(URL_PATTERN, v)
|
||||
if url is None:
|
||||
raise SettingValueError(
|
||||
setting_name=self.name,
|
||||
setting_value=v,
|
||||
rule="has to be a valid url"
|
||||
)
|
||||
|
||||
def single_object_from_element(self, value: str):
|
||||
return urlparse(value)
|
||||
|
||||
|
||||
|
||||
class ConnectionSection(Section):
|
||||
def __init__(self):
|
||||
self.PROXIES = ProxAttribute(
|
||||
name="proxies",
|
||||
description="Set your proxies.\n"
|
||||
"Must be valid for http, as well as https.",
|
||||
value=[]
|
||||
)
|
||||
|
||||
self.USE_TOR = BoolAttribute(
|
||||
name="tor",
|
||||
description="Route ALL traffic through Tor.\n"
|
||||
"If you use Tor, make sure the Tor browser is installed, and running."
|
||||
"I can't guarantee maximum security though!",
|
||||
value="false"
|
||||
)
|
||||
self.TOR_PORT = IntAttribute(
|
||||
name="tor_port",
|
||||
description="The port, tor is listening. If tor is already working, don't change it.",
|
||||
value="9150"
|
||||
)
|
||||
self.CHUNK_SIZE = IntAttribute(
|
||||
name="chunk_size",
|
||||
description="Size of the chunks that are streamed.",
|
||||
value="1024"
|
||||
)
|
||||
self.SHOW_DOWNLOAD_ERRORS_THRESHOLD = FloatAttribute(
|
||||
name="show_download_errors_threshold",
|
||||
description="If the percentage of failed downloads goes over this threshold,\n"
|
||||
"all the error messages are shown.",
|
||||
value="0.3"
|
||||
)
|
||||
|
||||
# INVIDIOUS INSTANCES LIST
|
||||
self.INVIDIOUS_INSTANCE = UrlStringAttribute(
|
||||
name="invidious_instance",
|
||||
description="This is an attribute, where you can define the invidious instances,\n"
|
||||
"the youtube downloader should use.\n"
|
||||
"Here is a list of active ones: https://docs.invidious.io/instances/\n"
|
||||
"Instances that use cloudflare or have source code changes could cause issues.\n"
|
||||
"Hidden instances (.onion) will only work, when setting 'tor=true'.",
|
||||
value="https://yt.artemislena.eu/"
|
||||
)
|
||||
|
||||
self.PIPED_INSTANCE = UrlStringAttribute(
|
||||
name="piped_instance",
|
||||
description="This is an attribute, where you can define the pioed instances,\n"
|
||||
"the youtube downloader should use.\n"
|
||||
"Here is a list of active ones: https://github.com/TeamPiped/Piped/wiki/Instances\n"
|
||||
"Instances that use cloudflare or have source code changes could cause issues.\n"
|
||||
"Hidden instances (.onion) will only work, when setting 'tor=true'.",
|
||||
value="https://pipedapi.kavin.rocks"
|
||||
)
|
||||
|
||||
self.SLEEP_AFTER_YOUTUBE_403 = FloatAttribute(
|
||||
name="sleep_after_youtube_403",
|
||||
description="The time to wait, after youtube returned 403 (in seconds)",
|
||||
value="20"
|
||||
)
|
||||
|
||||
self.YOUTUBE_MUSIC_API_KEY = StringAttribute(
|
||||
name="youtube_music_api_key",
|
||||
description="This is the API key used by YouTube-Music internally.\nDw. if it is empty, Rachel will fetch it automatically for you <333\n(she will also update outdated api keys/those that don't work)",
|
||||
value="AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
||||
)
|
||||
|
||||
self.YOUTUBE_MUSIC_CLEAN_DATA = BoolAttribute(
|
||||
name="youtube_music_clean_data",
|
||||
description="If set to true, it exclusively fetches artists/albums/songs, not things like user channels etc.",
|
||||
value="true"
|
||||
)
|
||||
|
||||
self.ALL_YOUTUBE_URLS = UrlListAttribute(
|
||||
name="youtube_url",
|
||||
description="This is used to detect, if an url is from youtube, or any alternativ frontend.\n"
|
||||
"If any instance seems to be missing, run music kraken with the -f flag.",
|
||||
value=[
|
||||
"https://www.youtube.com/",
|
||||
"https://www.youtu.be/",
|
||||
"https://redirect.invidious.io/",
|
||||
"https://piped.kavin.rocks/"
|
||||
]
|
||||
)
|
||||
|
||||
self.SPONSOR_BLOCK = BoolAttribute(
|
||||
name="use_sponsor_block",
|
||||
value="true",
|
||||
description="Use sponsor block to remove adds or simmilar from the youtube videos."
|
||||
)
|
||||
|
||||
self.attribute_list = [
|
||||
self.USE_TOR,
|
||||
self.TOR_PORT,
|
||||
self.CHUNK_SIZE,
|
||||
self.SHOW_DOWNLOAD_ERRORS_THRESHOLD,
|
||||
self.INVIDIOUS_INSTANCE,
|
||||
self.PIPED_INSTANCE,
|
||||
self.SLEEP_AFTER_YOUTUBE_403,
|
||||
self.YOUTUBE_MUSIC_API_KEY,
|
||||
self.YOUTUBE_MUSIC_CLEAN_DATA,
|
||||
self.ALL_YOUTUBE_URLS,
|
||||
self.SPONSOR_BLOCK
|
||||
]
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
CONNECTION_SECTION = ConnectionSection()
|
130
src/music_kraken/utils/config/sections/logging.py
Normal file
130
src/music_kraken/utils/config/sections/logging.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from ..base_classes import SingleAttribute, StringAttribute, Section, Description, EmptyLine
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": 50,
|
||||
"ERROR": 40,
|
||||
"WARNING": 30,
|
||||
"INFO": 20,
|
||||
"DEBUG": 10,
|
||||
"NOTSET": 0
|
||||
}
|
||||
|
||||
|
||||
class LoggerAttribute(SingleAttribute):
|
||||
@property
|
||||
def object_from_value(self) -> logging.Logger:
|
||||
return logging.getLogger(self.value)
|
||||
|
||||
|
||||
class LogLevelAttribute(SingleAttribute):
|
||||
@property
|
||||
def object_from_value(self) -> int:
|
||||
"""
|
||||
gets the numeric value of a log level
|
||||
:return:
|
||||
"""
|
||||
if self.value.isnumeric():
|
||||
return int(self.value)
|
||||
|
||||
v = self.value.strip().upper()
|
||||
|
||||
if v not in LOG_LEVELS:
|
||||
raise ValueError(
|
||||
f"{self.name} can only been either one of the following levels, or an integer:\n"
|
||||
f"{';'.join(key for key in LOG_LEVELS)}"
|
||||
)
|
||||
|
||||
return LOG_LEVELS[v]
|
||||
|
||||
|
||||
class LoggingSection(Section):
|
||||
def __init__(self):
|
||||
self.FORMAT = StringAttribute(
|
||||
name="logging_format",
|
||||
description="Reference for the logging formats: "
|
||||
"https://docs.python.org/3/library/logging.html#logrecord-attributes",
|
||||
value=logging.BASIC_FORMAT
|
||||
)
|
||||
self.LOG_LEVEL = LogLevelAttribute(
|
||||
name="log_level",
|
||||
description=f"can only been either one of the following levels, or an integer:\n"
|
||||
f"{';'.join(key for key in LOG_LEVELS)}",
|
||||
value=str(logging.INFO)
|
||||
)
|
||||
|
||||
self.DOWNLOAD_LOGGER = LoggerAttribute(
|
||||
name="download_logger",
|
||||
description="The logger for downloading.",
|
||||
value="download"
|
||||
)
|
||||
self.TAGGING_LOGGER = LoggerAttribute(
|
||||
name="tagging_logger",
|
||||
description="The logger for tagging id3 containers.",
|
||||
value="tagging"
|
||||
)
|
||||
self.CODEX_LOGGER = LoggerAttribute(
|
||||
name="codex_logger",
|
||||
description="The logger for streaming the audio into an uniform codex.",
|
||||
value="codex"
|
||||
)
|
||||
self.OBJECT_LOGGER = LoggerAttribute(
|
||||
name="object_logger",
|
||||
description="The logger for creating Data-Objects.",
|
||||
value="object"
|
||||
)
|
||||
self.DATABASE_LOGGER = LoggerAttribute(
|
||||
name="database_logger",
|
||||
description="The logger for Database operations.",
|
||||
value="database"
|
||||
)
|
||||
self.MUSIFY_LOGGER = LoggerAttribute(
|
||||
name="musify_logger",
|
||||
description="The logger for the musify scraper.",
|
||||
value="musify"
|
||||
)
|
||||
self.YOUTUBE_LOGGER = LoggerAttribute(
|
||||
name="youtube_logger",
|
||||
description="The logger for the youtube scraper.",
|
||||
value="youtube"
|
||||
)
|
||||
self.YOUTUBE_MUSIC_LOGGER = LoggerAttribute(
|
||||
name="youtube_music_logger",
|
||||
description="The logger for the youtube music scraper.\n(The scraper is seperate to the youtube scraper)",
|
||||
value="youtube_music"
|
||||
)
|
||||
self.ENCYCLOPAEDIA_METALLUM_LOGGER = LoggerAttribute(
|
||||
name="metal_archives_logger",
|
||||
description="The logger for the metal archives scraper.",
|
||||
value="metal_archives"
|
||||
)
|
||||
self.GENIUS_LOGGER = LoggerAttribute(
|
||||
name="genius_logger",
|
||||
description="The logger for the genius scraper",
|
||||
value="genius"
|
||||
)
|
||||
|
||||
self.attribute_list = [
|
||||
Description("Logging settings for the actual logging:"),
|
||||
self.FORMAT,
|
||||
self.LOG_LEVEL,
|
||||
EmptyLine(),
|
||||
Description("Just the names for different logger, for different parts of the programm:"),
|
||||
self.DOWNLOAD_LOGGER,
|
||||
self.TAGGING_LOGGER,
|
||||
self.CODEX_LOGGER,
|
||||
self.OBJECT_LOGGER,
|
||||
self.DATABASE_LOGGER,
|
||||
self.MUSIFY_LOGGER,
|
||||
self.YOUTUBE_LOGGER,
|
||||
self.YOUTUBE_MUSIC_LOGGER,
|
||||
self.ENCYCLOPAEDIA_METALLUM_LOGGER,
|
||||
self.GENIUS_LOGGER
|
||||
]
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
LOGGING_SECTION = LoggingSection()
|
72
src/music_kraken/utils/config/sections/misc.py
Normal file
72
src/music_kraken/utils/config/sections/misc.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from ..base_classes import Section, IntAttribute, ListAttribute, BoolAttribute
|
||||
|
||||
|
||||
class MiscSection(Section):
|
||||
def __init__(self):
|
||||
self.HASNT_YET_STARTED = BoolAttribute(
|
||||
name="hasnt_yet_started",
|
||||
description="If you did already run, and configured everything, this is false.",
|
||||
value="true"
|
||||
)
|
||||
|
||||
self.ENABLE_RESULT_HISTORY = BoolAttribute(
|
||||
name="result_history",
|
||||
description="If enabled, you can go back to the previous results.\n"
|
||||
"The consequence is a higher meory consumption, because every result is saved.",
|
||||
value="false"
|
||||
)
|
||||
|
||||
self.HISTORY_LENGTH = IntAttribute(
|
||||
name="history_length",
|
||||
description="You can choose how far back you can go in the result history.\n"
|
||||
"The further you choose to be able to go back, the higher the memory usage.\n"
|
||||
"'-1' removes the Limit entirely.",
|
||||
value="8"
|
||||
)
|
||||
|
||||
self.HAPPY_MESSAGES = ListAttribute(
|
||||
name="happy_messages",
|
||||
description="Just some nice and wholesome messages.\n"
|
||||
"If your mindset has traits of a [file corruption], you might not agree.\n"
|
||||
"But anyways... Freedom of thought, so go ahead and change the messages.",
|
||||
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",
|
||||
"Gotta love the BPJM ;-;",
|
||||
"🏳️⚧️🏳️⚧️ Protect trans youth. 🏳️⚧️🏳️⚧️",
|
||||
]
|
||||
)
|
||||
|
||||
self.MODIFY_GC = BoolAttribute(
|
||||
name="modify_gc",
|
||||
description="If set to true, it will modify the gc for the sake of performance.\n"
|
||||
"This should not drive up ram usage, but if it is, then turn it of.\n"
|
||||
"Here a blog post about that matter:\n"
|
||||
"https://mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/\n"
|
||||
"https://web.archive.org/web/20221124122222/https://mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/",
|
||||
value="true"
|
||||
)
|
||||
|
||||
self.ID_BITS = IntAttribute(
|
||||
name="id_bits",
|
||||
description="I really dunno why I even made this a setting.. Modifying this is a REALLY dumb idea.",
|
||||
value="64"
|
||||
)
|
||||
|
||||
self.attribute_list = [
|
||||
self.HASNT_YET_STARTED,
|
||||
self.ENABLE_RESULT_HISTORY,
|
||||
self.HISTORY_LENGTH,
|
||||
self.HAPPY_MESSAGES,
|
||||
self.MODIFY_GC,
|
||||
self.ID_BITS
|
||||
]
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
MISC_SECTION = MiscSection()
|
59
src/music_kraken/utils/config/sections/paths.py
Normal file
59
src/music_kraken/utils/config/sections/paths.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from pathlib import Path
|
||||
|
||||
from ...path_manager import LOCATIONS
|
||||
from ..base_classes import Section, StringAttribute, ListAttribute
|
||||
|
||||
|
||||
class PathAttribute(StringAttribute):
|
||||
@property
|
||||
def object_from_value(self) -> Path:
|
||||
return Path(self.value.strip())
|
||||
|
||||
|
||||
class PathsSection(Section):
|
||||
def __init__(self):
|
||||
self.MUSIC_DIRECTORY = PathAttribute(
|
||||
name="music_directory",
|
||||
description="The directory, all the music will be downloaded to.",
|
||||
value=str(LOCATIONS.MUSIC_DIRECTORY)
|
||||
)
|
||||
|
||||
self.TEMP_DIRECTORY = PathAttribute(
|
||||
name="temp_directory",
|
||||
description="All temporary stuff is gonna be dumped in this directory.",
|
||||
value=str(LOCATIONS.TEMP_DIRECTORY)
|
||||
)
|
||||
|
||||
self.LOG_PATH = PathAttribute(
|
||||
name="log_file",
|
||||
description="The path to the logging file",
|
||||
value=str(LOCATIONS.get_log_file("download_logs.log"))
|
||||
)
|
||||
|
||||
self.NOT_A_GENRE_REGEX = ListAttribute(
|
||||
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",
|
||||
value=[
|
||||
r'^\.' # is hidden/starts with a "."
|
||||
]
|
||||
)
|
||||
|
||||
self.FFMPEG_BINARY = PathAttribute(
|
||||
name="ffmpeg_binary",
|
||||
description="Set the path to the ffmpeg binary.",
|
||||
value=str(LOCATIONS.FFMPEG_BIN)
|
||||
)
|
||||
|
||||
self.attribute_list = [
|
||||
self.MUSIC_DIRECTORY,
|
||||
self.TEMP_DIRECTORY,
|
||||
self.LOG_PATH,
|
||||
self.NOT_A_GENRE_REGEX,
|
||||
self.FFMPEG_BINARY
|
||||
]
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
PATHS_SECTION = PathsSection()
|
4
src/music_kraken/utils/config/utils.py
Normal file
4
src/music_kraken/utils/config/utils.py
Normal file
@@ -0,0 +1,4 @@
|
||||
def comment(uncommented_string: str) -> str:
|
||||
_fragments = uncommented_string.split("\n")
|
||||
_fragments = ["# " + frag for frag in _fragments]
|
||||
return "\n".join(_fragments)
|
Reference in New Issue
Block a user