feat: build
This commit is contained in:
33
music_kraken/utils/config/__init__.py
Normal file
33
music_kraken/utils/config/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from typing import Tuple
|
||||
|
||||
from .config import Config
|
||||
from .config_files import (
|
||||
main_config,
|
||||
logging_config,
|
||||
youtube_config,
|
||||
)
|
||||
|
||||
_sections: Tuple[Config, ...] = (
|
||||
main_config.config,
|
||||
logging_config.config,
|
||||
youtube_config.config
|
||||
)
|
||||
|
||||
def read_config():
|
||||
for section in _sections:
|
||||
section.read()
|
||||
|
||||
# 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()
|
||||
|
||||
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
|
0
music_kraken/utils/config/attributes/__init__.py
Normal file
0
music_kraken/utils/config/attributes/__init__.py
Normal file
132
music_kraken/utils/config/attributes/attribute.py
Normal file
132
music_kraken/utils/config/attributes/attribute.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import re
|
||||
from typing import Optional, List, Union, Iterable, Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import toml
|
||||
from copy import deepcopy, copy
|
||||
from urllib.parse import urlparse, urlunparse, ParseResult
|
||||
|
||||
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:
|
||||
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:
|
||||
description: str
|
||||
|
||||
@property
|
||||
def toml_string(self):
|
||||
return comment_string(self.description)
|
||||
|
||||
|
||||
class EmptyLine(Description):
|
||||
def __init__(self):
|
||||
self.description = ""
|
||||
|
||||
|
||||
|
||||
class Attribute:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
default_value: any,
|
||||
description: Optional[str] = None,
|
||||
):
|
||||
|
||||
self.name = name
|
||||
|
||||
self.value = self._recursive_parse_object(default_value, self.parse_simple_value)
|
||||
|
||||
self.description: Optional[str] = description
|
||||
self.loaded_settings: dict = None
|
||||
|
||||
def initialize_from_config(self, loaded_settings: dict):
|
||||
self.loaded_settings = loaded_settings
|
||||
self.loaded_settings.__setitem__(self.name, self.value, True)
|
||||
|
||||
def unparse_simple_value(self, value: any) -> any:
|
||||
return value
|
||||
|
||||
def parse_simple_value(self, value: any) -> any:
|
||||
return value
|
||||
|
||||
def _recursive_parse_object(self, __object, callback: Callable):
|
||||
__object = copy(__object)
|
||||
|
||||
if isinstance(__object, dict):
|
||||
for key, value in __object.items():
|
||||
__object[key] = self._recursive_parse_object(value, callback)
|
||||
|
||||
return __object
|
||||
|
||||
if isinstance(__object, list) or (isinstance(__object, tuple) and not isinstance(__object, ParseResult)):
|
||||
for i, item in enumerate(__object):
|
||||
__object[i] = self._recursive_parse_object(item, callback)
|
||||
return __object
|
||||
|
||||
return callback(__object)
|
||||
|
||||
def parse(self, unparsed_value):
|
||||
self.value = self._recursive_parse_object(unparsed_value, self.parse_simple_value)
|
||||
return self.value
|
||||
|
||||
def unparse(self, parsed_value):
|
||||
return self._recursive_parse_object(parsed_value, self.unparse_simple_value)
|
||||
|
||||
def load_toml(self, loaded_toml: 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.")
|
||||
self.loaded_settings.__setitem__(self.name, self.value, True)
|
||||
return
|
||||
|
||||
try:
|
||||
self.parse(loaded_toml[self.name])
|
||||
except SettingValueError as settings_error:
|
||||
logging.warning(settings_error)
|
||||
return False
|
||||
|
||||
self.loaded_settings.__setitem__(self.name, self.value, True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@property
|
||||
def toml_string(self) -> str:
|
||||
string = ""
|
||||
|
||||
if self.description is not None:
|
||||
string += comment(self.description) + "\n"
|
||||
|
||||
string += toml.dumps({self.name: self.unparse(self.value)})
|
||||
|
||||
# print(string)
|
||||
return string
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.description}\n{self.name}={self.value}"
|
||||
|
151
music_kraken/utils/config/attributes/special_attributes.py
Normal file
151
music_kraken/utils/config/attributes/special_attributes.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from pathlib import Path, PosixPath
|
||||
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) -> Path:
|
||||
if isinstance(value, Path) or isinstance(value, PosixPath):
|
||||
return value
|
||||
return Path(value)
|
||||
|
||||
def unparse_simple_value(self, value: Path) -> any:
|
||||
return str(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(value)
|
||||
|
||||
def unparse_simple_value(self, value: logging.Logger) -> any:
|
||||
return value.name
|
85
music_kraken/utils/config/config.py
Normal file
85
music_kraken/utils/config/config.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from typing import Any, Tuple, Union, List
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import toml
|
||||
|
||||
from .attributes.attribute import Attribute, Description, EmptyLine
|
||||
|
||||
|
||||
class ConfigDict(dict):
|
||||
def __init__(self, config_reference: "Config", *args, **kwargs):
|
||||
self.config_reference: Config = config_reference
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, __name: str) -> Any:
|
||||
return super().__getitem__(__name)
|
||||
|
||||
def __setitem__(self, __key: Any, __value: Any, from_attribute: bool = False, is_parsed: bool = False) -> None:
|
||||
if not from_attribute:
|
||||
attribute: Attribute = self.config_reference.attribute_map[__key]
|
||||
if is_parsed:
|
||||
attribute.value = __value
|
||||
else:
|
||||
attribute.parse(__value)
|
||||
self.config_reference.write()
|
||||
|
||||
__value = attribute.value
|
||||
|
||||
return super().__setitem__(__key, __value)
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, component_list: Tuple[Union[Attribute, Description, EmptyLine], ...], config_file: Path) -> None:
|
||||
self.config_file: Path = config_file
|
||||
|
||||
self.component_list: List[Union[Attribute, Description, EmptyLine]] = [
|
||||
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()}
|
||||
|
||||
_____
|
||||
/ ____|
|
||||
| | __ __ _ _ _
|
||||
| | |_ | / _` || | | |
|
||||
| |__| || (_| || |_| |
|
||||
\_____| \__,_| \__, |
|
||||
__/ |
|
||||
|___/
|
||||
""")]
|
||||
|
||||
self.component_list.extend(component_list)
|
||||
self.loaded_settings: ConfigDict = ConfigDict(self)
|
||||
|
||||
self.attribute_map = {}
|
||||
for component in self.component_list:
|
||||
if not isinstance(component, Attribute):
|
||||
continue
|
||||
|
||||
component.initialize_from_config(self.loaded_settings)
|
||||
self.attribute_map[component.name] = component
|
||||
|
||||
@property
|
||||
def toml_string(self):
|
||||
return "\n".join(component.toml_string for component in self.component_list)
|
||||
|
||||
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)
|
||||
|
||||
for component in self.component_list:
|
||||
if isinstance(component, Attribute):
|
||||
component.load_toml(toml_data)
|
0
music_kraken/utils/config/config_files/__init__.py
Normal file
0
music_kraken/utils/config/config_files/__init__.py
Normal file
105
music_kraken/utils/config/config_files/logging_config.py
Normal file
105
music_kraken/utils/config/config_files/logging_config.py
Normal file
@@ -0,0 +1,105 @@
|
||||
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=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"
|
||||
),
|
||||
LoggerAttribute(
|
||||
name="bandcamp_logger",
|
||||
description="The logger for the bandcamp scraper",
|
||||
default_value="bandcamp"
|
||||
)
|
||||
|
||||
], 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
|
||||
bandcamp_logger: Logger
|
153
music_kraken/utils/config/config_files/main_config.py
Normal file
153
music_kraken/utils/config/config_files/main_config.py
Normal file
@@ -0,0 +1,153 @@
|
||||
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, EmptyLine, Description
|
||||
from ..attributes.special_attributes import (
|
||||
SelectAttribute,
|
||||
PathAttribute,
|
||||
AudioFormatAttribute
|
||||
)
|
||||
|
||||
config = Config((
|
||||
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."""),
|
||||
|
||||
Attribute(
|
||||
name="language",
|
||||
default_value="en-US,en;q=0.6",
|
||||
description="The language of the program. This will be used to translate the program in the future.\n"
|
||||
"Currently it just sets the Accept-Language header.\n"
|
||||
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language"
|
||||
),
|
||||
Attribute(
|
||||
name="user_agent",
|
||||
default_value="Mozilla/5.0 (X11; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0",
|
||||
description="The user agent of the program. This will be used to translate the program in the future.\n"
|
||||
"Currently it just sets the User-Agent header.\n"
|
||||
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent"
|
||||
),
|
||||
Attribute(
|
||||
name="tries_per_proxy",
|
||||
default_value=2,
|
||||
description="The retries it should do. These can be overridden by the program, at certain places, and they have to be.",
|
||||
),
|
||||
|
||||
EmptyLine(),
|
||||
|
||||
PathAttribute(name="music_directory", default_value=LOCATIONS.MUSIC_DIRECTORY.resolve(), description="The directory, all the music will be downloaded to."),
|
||||
PathAttribute(name="temp_directory", default_value=LOCATIONS.TEMP_DIRECTORY.resolve(), description="All temporary stuff is gonna be dumped in this directory."),
|
||||
PathAttribute(name="log_file", default_value=LOCATIONS.get_log_file("download_logs.log").resolve()),
|
||||
PathAttribute(name="ffmpeg_binary", default_value=LOCATIONS.FFMPEG_BIN.resolve(), description="Set the path to the ffmpeg binary."),
|
||||
PathAttribute(name="cache_directory", default_value=LOCATIONS.CACHE_DIRECTORY.resolve(), description="Set the path of the cache directory."),
|
||||
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="modify_gc", default_value=True),
|
||||
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]]
|
||||
tries_per_proxy: int
|
||||
tor: bool
|
||||
tor_port: int
|
||||
chunk_size: int
|
||||
show_download_errors_threshold: float
|
||||
language: str
|
||||
user_agent: str
|
||||
|
||||
# paths
|
||||
music_directory: Path
|
||||
temp_directory: Path
|
||||
log_file: Path
|
||||
not_a_genre_regex: List[str]
|
||||
ffmpeg_binary: Path
|
||||
cache_directory: Path
|
113
music_kraken/utils/config/config_files/youtube_config.py
Normal file
113
music_kraken/utils/config/config_files/youtube_config.py
Normal file
@@ -0,0 +1,113 @@
|
||||
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((
|
||||
Attribute(name="use_youtube_alongside_youtube_music", default_value=False, description="""If set to true, it will search youtube through invidious and piped,
|
||||
despite a direct wrapper for the youtube music INNERTUBE api being implemented.
|
||||
I my INNERTUBE api wrapper doesn't work, set this to true."""),
|
||||
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/",
|
||||
"https://music.youtube.com/",
|
||||
], 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."),
|
||||
|
||||
Attribute(name="player_url", default_value="https://music.youtube.com/s/player/80b90bfd/player_ias.vflset/en_US/base.js", description="""
|
||||
This is needed to fetch videos without invidious
|
||||
"""),
|
||||
Attribute(name="youtube_music_consent_cookies", default_value={
|
||||
"CONSENT": "PENDING+258"
|
||||
}, description="The cookie with the key CONSENT says to what stuff you agree. Per default you decline all cookies, but it honestly doesn't matter."),
|
||||
|
||||
Attribute(name="youtube_music_innertube_context", default_value={
|
||||
"client": {
|
||||
"hl": "en",
|
||||
"gl": "DE",
|
||||
"remoteHost": "87.123.241.77",
|
||||
"deviceMake": "",
|
||||
"deviceModel": "",
|
||||
"visitorData": "CgtiTUxaTHpoXzk1Zyia59WlBg%3D%3D",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
"clientName": "WEB_REMIX",
|
||||
"clientVersion": "1.20230710.01.00",
|
||||
"osName": "X11",
|
||||
"osVersion": "",
|
||||
"originalUrl": "https://music.youtube.com/",
|
||||
"platform": "DESKTOP",
|
||||
"clientFormFactor": "UNKNOWN_FORM_FACTOR",
|
||||
"configInfo": {
|
||||
"appInstallData": "",
|
||||
"coldConfigData": "",
|
||||
"coldHashData": "",
|
||||
"hotHashData": ""
|
||||
},
|
||||
"userInterfaceTheme": "USER_INTERFACE_THEME_DARK",
|
||||
"timeZone": "Atlantic/Jan_Mayen",
|
||||
"browserName": "Firefox",
|
||||
"browserVersion": "115.0",
|
||||
"acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"deviceExperimentId": "ChxOekkxTmpnek16UTRNVFl4TkRrek1ETTVOdz09EJrn1aUGGJrn1aUG",
|
||||
"screenWidthPoints": 584,
|
||||
"screenHeightPoints": 939,
|
||||
"screenPixelDensity": 1,
|
||||
"screenDensityFloat": 1,
|
||||
"utcOffsetMinutes": 120,
|
||||
"musicAppInfo": {
|
||||
"pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN",
|
||||
"webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
|
||||
"storeDigitalGoodsApiSupportStatus": {
|
||||
"playStoreDigitalGoodsApiSupportStatus": "DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": { "lockedSafetyMode": False },
|
||||
"request": {
|
||||
"useSsl": True,
|
||||
"internalExperimentFlags": [],
|
||||
"consistencyTokenJars": []
|
||||
},
|
||||
"adSignalsInfo": {
|
||||
"params": []
|
||||
}
|
||||
}, description="Don't bother about this. It is something technical, but if you wanna change the innertube requests... go on."),
|
||||
Attribute(name="ytcfg", description="Please... ignore it.", default_value={})
|
||||
), LOCATIONS.get_config_file("youtube"))
|
||||
|
||||
|
||||
class SettingsStructure(TypedDict):
|
||||
use_youtube_alongside_youtube_music: bool
|
||||
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
|
||||
player_url: str
|
||||
youtube_music_innertube_context: dict
|
||||
youtube_music_consent_cookies: dict
|
||||
ytcfg: dict
|
4
music_kraken/utils/config/utils.py
Normal file
4
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