From cef87460a7ca208ed8c00e55292d05b4ffc6ced1 Mon Sep 17 00:00:00 2001 From: Lars Noack Date: Fri, 24 May 2024 14:46:38 +0200 Subject: [PATCH] draft --- .vscode/launch.json | 6 + music_kraken/cli/main_downloader.py | 3 +- music_kraken/download/__init__.py | 109 +++++++++++++++--- music_kraken/download/results.py | 8 +- .../pages/_youtube_music/super_youtube.py | 25 ++-- music_kraken/utils/exception/__init__.py | 3 + 6 files changed, 119 insertions(+), 35 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 24c2088..f864249 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,12 @@ "request": "launch", "program": "development/actual_donwload.py", "console": "integratedTerminal" + }, + { + "name": "Python Debugger: Music Kraken", + "type": "debugpy", + "request": "launch", // run the module + "module": "music_kraken", } ] } \ No newline at end of file diff --git a/music_kraken/cli/main_downloader.py b/music_kraken/cli/main_downloader.py index 2356f8e..b21b265 100644 --- a/music_kraken/cli/main_downloader.py +++ b/music_kraken/cli/main_downloader.py @@ -4,10 +4,9 @@ from pathlib import Path from typing import Dict, List, Set, Type from .. import console -from ..download import Downloader +from ..download import Downloader, Page from ..download.results import GoToResults, Option, PageResults, Results from ..objects import Album, Artist, DatabaseObject, Song -from ..pages import Page from ..utils import BColors, output from ..utils.config import main_settings, write_config from ..utils.enums.colors import BColors diff --git a/music_kraken/download/__init__.py b/music_kraken/download/__init__.py index 2a07672..3aa27fc 100644 --- a/music_kraken/download/__init__.py +++ b/music_kraken/download/__init__.py @@ -8,8 +8,8 @@ from copy import copy from dataclasses import dataclass, field from pathlib import Path from string import Formatter -from typing import (TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, - TypedDict, Union) +from typing import (TYPE_CHECKING, Any, Callable, Dict, Generator, List, + Optional, Set, Tuple, Type, TypedDict, Union) import requests from bs4 import BeautifulSoup @@ -23,7 +23,7 @@ from ..utils import BColors, output, trace from ..utils.config import main_settings, youtube_settings from ..utils.enums import ALL_SOURCE_TYPES, SourceType from ..utils.enums.album import AlbumType -from ..utils.exception import MKMissingNameException +from ..utils.exception import MKComposeException, MKMissingNameException from ..utils.exception.download import UrlNotFoundException from ..utils.path_manager import LOCATIONS from ..utils.shared import DEBUG_PAGES @@ -75,7 +75,7 @@ class Downloader: self.scan_for_pages(**kwargs) def register_page(self, page_type: Type[Page], **kwargs): - if page_type in _registered_pages: + if page_type in self._registered_pages: return self._registered_pages[page_type].add(page_type( @@ -343,13 +343,15 @@ class Page: return super().__new__(cls) def __init__(self, download_options: DownloadOptions = None, fetch_options: FetchOptions = None, **kwargs): - self.SOURCE_TYPE.register_page(self) + if self.SOURCE_TYPE is not None: + self.SOURCE_TYPE.register_page(self) self.download_options: DownloadOptions = download_options or DownloadOptions() self.fetch_options: FetchOptions = fetch_options or FetchOptions() def __del__(self): - self.SOURCE_TYPE.deregister_page() + if self.SOURCE_TYPE is not None: + self.SOURCE_TYPE.deregister_page() def _search_regex(self, pattern, string, default=None, fatal=True, flags=0, group=None): """ @@ -451,27 +453,106 @@ class Option: This could represent a data object, a string or a page. """ - def __init__(self, value: Any, text: Optional[str] = None, keys: Set[str] = None, hidden: bool = False): + 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.keys = keys or set() - self.keys.add(self.text) + 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 SelectOption: - def __init__(self, options: List[Option] = None): +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 + self._options: List[Option] = [] - self.extend(options or []) + 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) \ No newline at end of file + 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/results.py b/music_kraken/download/results.py index c6ac522..3587da4 100644 --- a/music_kraken/download/results.py +++ b/music_kraken/download/results.py @@ -1,8 +1,12 @@ +from __future__ import annotations + from dataclasses import dataclass -from typing import Dict, Generator, List, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, Generator, List, Tuple, Type, Union from ..objects import DatabaseObject -from . import Page + +if TYPE_CHECKING: + from . import Page @dataclass diff --git a/music_kraken/pages/_youtube_music/super_youtube.py b/music_kraken/pages/_youtube_music/super_youtube.py index fa5ce1c..3e3dced 100644 --- a/music_kraken/pages/_youtube_music/super_youtube.py +++ b/music_kraken/pages/_youtube_music/super_youtube.py @@ -1,26 +1,17 @@ -from typing import List, Optional, Type, Tuple -from urllib.parse import urlparse, urlunparse, parse_qs from enum import Enum -import requests +from typing import List, Optional, Tuple, Type +from urllib.parse import parse_qs, urlparse, urlunparse import python_sponsorblock +import requests -from ...objects import Source, DatabaseObject, Song, Target -from .._abstract import Page -from ...objects import ( - Artist, - Source, - Song, - Album, - Label, - Target, - FormattedText, - ID3Timestamp -) from ...connection import Connection +from ...download import Page +from ...objects import (Album, Artist, DatabaseObject, FormattedText, + ID3Timestamp, Label, Song, Source, Target) +from ...utils.config import logging_settings, main_settings, youtube_settings +from ...utils.enums import ALL_SOURCE_TYPES, SourceType from ...utils.support_classes.download_result import DownloadResult -from ...utils.config import youtube_settings, logging_settings, main_settings -from ...utils.enums import SourceType, ALL_SOURCE_TYPES def get_invidious_url(path: str = "", params: str = "", query: str = "", fragment: str = "") -> str: diff --git a/music_kraken/utils/exception/__init__.py b/music_kraken/utils/exception/__init__.py index 8f139fb..42b26d7 100644 --- a/music_kraken/utils/exception/__init__.py +++ b/music_kraken/utils/exception/__init__.py @@ -3,6 +3,9 @@ class MKBaseException(Exception): self.message = message super().__init__(message, **kwargs) +# Compose exceptions. Those usually mean a bug on my side. +class MKComposeException(MKBaseException): + pass # Downloading class MKDownloadException(MKBaseException):