diff --git a/music_kraken/cli/main_downloader.py b/music_kraken/cli/main_downloader.py index b21b265..9bc0b37 100644 --- a/music_kraken/cli/main_downloader.py +++ b/music_kraken/cli/main_downloader.py @@ -1,11 +1,13 @@ import random import re from pathlib import Path -from typing import Dict, List, Set, Type +from typing import Dict, Generator, List, Set, Type from .. import console -from ..download import Downloader, Page -from ..download.results import GoToResults, Option, PageResults, Results +from ..download import Downloader, Page, components +from ..download.results import GoToResults +from ..download.results import Option as ResultOption +from ..download.results import PageResults, Results from ..objects import Album, Artist, DatabaseObject, Song from ..utils import BColors, output from ..utils.config import main_settings, write_config @@ -17,7 +19,7 @@ from ..utils.string_processing import fit_to_file_system from ..utils.support_classes.download_result import DownloadResult from ..utils.support_classes.query import Query from .options.first_config import initial_config -from .utils import cli_function +from .utils import ask_for_bool, cli_function EXIT_COMMANDS = {"q", "quit", "exit", "abort"} ALPHABET = "abcdefghijklmnopqrstuvwxyz" @@ -25,50 +27,19 @@ PAGE_NAME_FILL = "-" MAX_PAGE_LEN = 21 -def get_existing_genre() -> List[str]: - """ - gets the name of all subdirectories of shared.MUSIC_DIR, - but filters out all directories, where the name matches with any patern - from shared.NOT_A_GENRE_REGEX. - """ - existing_genres: List[str] = [] - - # get all subdirectories of MUSIC_DIR, not the files in the dir. - existing_subdirectories: List[Path] = [f for f in main_settings["music_directory"].iterdir() if f.is_dir()] - - for subdirectory in existing_subdirectories: - name: str = subdirectory.name - - if not any(re.match(regex_pattern, name) for regex_pattern in main_settings["not_a_genre_regex"]): - existing_genres.append(name) - - existing_genres.sort() - - return existing_genres - - def get_genre(): - existing_genres = get_existing_genre() - for i, genre_option in enumerate(existing_genres): - print(f"{i + 1:0>2}: {genre_option}") + select_genre = components.GenreSelect() + select_genre._ask_for_creating_option = lambda key: ask_for_bool(f"Create the genre \"{key}\"") - while True: - genre = input("Id or new genre: ") + genre: Optional[components.Option] = None - if genre.isdigit(): - genre_id = int(genre) - 1 - if genre_id >= len(existing_genres): - print(f"No genre under the id {genre_id + 1}.") - continue + while genre is None: + for genre in select_genre: + print(genre) - return existing_genres[genre_id] + genre = select_genre.choose(input("Id or new genre: ")) - new_genre = fit_to_file_system(genre) - - agree_inputs = {"y", "yes", "ok"} - verification = input(f"create new genre \"{new_genre}\"? (Y/N): ").lower() - if verification in agree_inputs: - return new_genre + return genre.value def help_message(): @@ -111,7 +82,7 @@ class CliDownloader: page_count = 0 for option in self.current_results.formatted_generator(): - if isinstance(option, Option): + if isinstance(option, ResultOption): r = f"{BColors.GREY.value}{option.index:0{self.option_digits}}{BColors.ENDC.value} {option.music_object.option_string}" print(r) else: diff --git a/music_kraken/cli/utils.py b/music_kraken/cli/utils.py index e2f9bab..cfcaadc 100644 --- a/music_kraken/cli/utils.py +++ b/music_kraken/cli/utils.py @@ -39,4 +39,8 @@ def print_cute_message(): print(message) +AGREE_INPUTS = {"y", "yes", "ok"} +def ask_for_bool(msg: str) -> bool: + i = input(msg + " (Y/N):").lower() + return i in AGREE_INPUTS \ No newline at end of file diff --git a/music_kraken/download/__init__.py b/music_kraken/download/__init__.py index 0d806b3..1a4aa6d 100644 --- a/music_kraken/download/__init__.py +++ b/music_kraken/download/__init__.py @@ -453,113 +453,3 @@ class Page: def download_song_to_target(self, source: Source, target: Target, desc: str = None) -> DownloadResult: return DownloadResult() - - -class Option: - """ - This could represent a data object, a string or a page. - """ - - def __init__( - self, - value: Any, - text: Optional[str] = None, - keys: List[Any] = None, - hidden: bool = False, - parse_key: Callable[[Any], Any] = lambda x: x, - ): - self._parse_key: Callable[[Any], Any] = parse_key - - self.value = value - self.text = text or str(value) - self.hidden = hidden - - self._raw_keys = set(keys or []) - self._raw_keys.add(self.text) - self.keys = set(self.parse_key(key) for key in self._raw_keys) - - def register_key(self, key: Any): - self._raw_keys.add(key) - self.keys.add(self._parse_key(key)) - - @property - def parse_key(self) -> Callable[[Any], Any]: - return self._parse_key - - @parse_key.setter - def parse_key(self, value: Callable[[Any], Any]): - self._parse_key = value - self.keys = set(self._parse_key(key) for key in self._raw_keys) - - def __str__(self): - return self.text - - -class Select: - def __init__( - self, - options: List[Option] = None, - option_factory: Callable[[Any], Option] = None, - raw_options: List[Any] = None, - parse_option_key: Callable[[Any], Any] = lambda x: x, - ask_for_creating_option: Callable[[Option], bool] = lambda x: True, - **kwargs - ): - self._parse_option_key: Callable[[Any], Any] = parse_option_key - self._ask_for_creating_option: Callable[[Option], bool] = ask_for_creating_option - - self._key_to_option: Dict[Any, Option] = dict() - self._options: List[Option] = [] - - options = options or [] - self.option_factory: Optional[Callable[[Any], Option]] = option_factory - if self.can_create_options: - for raw_option in raw_options or []: - self.append(self.option_factory(raw_option)) - elif raw_options is not None: - raise MKComposeException("Cannot create options without a factory.") - - self.extend(options) - - @property - def can_create_options(self) -> bool: - return self.option_factory is not None - - def append(self, option: Option): - option.parse_key = self._parse_option_key - self._options.append(option) - for key in option.keys: - self._key_to_option[key] = option - - def extend(self, options: List[Option]): - for option in options: - self.append(option) - - def __iter__(self) -> Generator[Option, None, None]: - for option in self._options: - if option.hidden: - continue - - yield option - - def __contains__(self, key: Any) -> bool: - return key in self._key_to_option - - def __getitem__(self, key: Any) -> Option: - return self._key_to_option[key] - - def create_option(self, key: Any, **kwargs) -> Option: - if not self.can_create_options: - raise MKComposeException("Cannot create options without a factory.") - - option = self.option_factory(key, **kwargs) - self.append(option) - return option - - def choose(self, key: Any) -> Optional[Option]: - if key not in self: - if self.can_create_options and self._ask_for_creating_option(key): - return self.create_option(key) - return None - - return self[key] diff --git a/music_kraken/download/components.py b/music_kraken/download/components.py new file mode 100644 index 0000000..e30bcb5 --- /dev/null +++ b/music_kraken/download/components.py @@ -0,0 +1,158 @@ +import re +from pathlib import Path +from typing import Any, Callable, Dict, Generator, List, Optional + +from ..utils.config import main_settings +from ..utils.exception import MKComposeException +from ..utils.string_processing import unify + + +class Option: + """ + This could represent a data object, a string or a page. + """ + + def __init__( + self, + value: Any, + text: Optional[str] = None, + keys: List[Any] = None, + hidden: bool = False, + parse_key: Callable[[Any], Any] = lambda x: x, + ): + self._parse_key: Callable[[Any], Any] = parse_key + + self.value = value + self.text = text or str(value) + self.hidden = hidden + + self._raw_keys = set(keys or []) + self._raw_keys.add(self.text) + self.keys = set(self.parse_key(key) for key in self._raw_keys) + + def register_key(self, key: Any): + self._raw_keys.add(key) + self.keys.add(self._parse_key(key)) + + @property + def parse_key(self) -> Callable[[Any], Any]: + return self._parse_key + + @parse_key.setter + def parse_key(self, value: Callable[[Any], Any]): + self._parse_key = value + self.keys = set(self._parse_key(key) for key in self._raw_keys) + + def __str__(self): + return self.text + + +class Select: + def __init__( + self, + options: Generator[Option, None, None] = None, + option_factory: Callable[[Any], Option] = None, + raw_options: List[Any] = None, + parse_option_key: Callable[[Any], Any] = lambda x: x, + ask_for_creating_option: Callable[[Option], bool] = lambda x: True, + sort: bool = False, + **kwargs + ): + self._parse_option_key: Callable[[Any], Any] = parse_option_key + self._ask_for_creating_option: Callable[[Option], bool] = ask_for_creating_option + + self._key_to_option: Dict[Any, Option] = dict() + self._options: List[Option] = [] + + options = options or [] + self.option_factory: Optional[Callable[[Any], Option]] = option_factory + if self.can_create_options: + _raw_options = raw_options or [] + if sort: + _raw_options = sorted(_raw_options) + + for raw_option in _raw_options: + self.append(self.option_factory(raw_option)) + elif raw_options is not None: + raise MKComposeException("Cannot create options without a factory.") + + self.extend(options) + + @property + def can_create_options(self) -> bool: + return self.option_factory is not None + + def append(self, option: Option): + option.parse_key = self._parse_option_key + self._options.append(option) + for key in option.keys: + self._key_to_option[key] = option + + def extend(self, options: List[Option]): + for option in options: + self.append(option) + + def __iter__(self) -> Generator[Option, None, None]: + for option in self._options: + if option.hidden: + continue + + yield option + + def __contains__(self, key: Any) -> bool: + return key in self._key_to_option + + def __getitem__(self, key: Any) -> Option: + return self._key_to_option[key] + + def create_option(self, key: Any, **kwargs) -> Option: + if not self.can_create_options: + raise MKComposeException("Cannot create options without a factory.") + + option = self.option_factory(key, **kwargs) + self.append(option) + return option + + def choose(self, key: Any) -> Optional[Option]: + if key not in self: + if self.can_create_options and self._ask_for_creating_option(key): + return self.create_option(key) + return None + + return self[key] + + + +class StringSelect(Select): + def __init__(self, **kwargs): + self._current_index = 0 + kwargs["option_factory"] = self.next_option + kwargs["parse_option_key"] = lambda x: unify(str(x)) + + super().__init__(**kwargs) + + def next_option(self, value: Any) -> Optional[Option]: + o = Option(value=value, keys=[self._current_index], text=f"{self._current_index:0>2}: {value}") + self._current_index += 1 + return o + + +class GenreSelect(StringSelect): + @staticmethod + def is_valid_genre(genre: Path) -> bool: + """ + gets the name of all subdirectories of shared.MUSIC_DIR, + but filters out all directories, where the name matches with any Patern + from shared.NOT_A_GENRE_REGEX. + """ + if not genre.is_dir(): + return False + + if any(re.match(regex_pattern, genre.name) for regex_pattern in main_settings["not_a_genre_regex"]): + return False + + return True + + def __init__(self): + super().__init__(sort=True, raw_options=(genre.name for genre in filter(self.is_valid_genre, main_settings["music_directory"].iterdir()))) +