diff --git a/grawlix/__main__.py b/grawlix/__main__.py index 6439a5d..95de72c 100644 --- a/grawlix/__main__.py +++ b/grawlix/__main__.py @@ -1,6 +1,6 @@ from .book import Book, Series from .config import load_config, Config, SourceConfig -from .exceptions import SourceNotAuthenticated +from .exceptions import SourceNotAuthenticated, GrawlixError from .sources import load_source, Source from .output import download_book from . import arguments, logging @@ -68,23 +68,27 @@ async def authenticate(source: Source, config: Config, options): async def main() -> None: args = arguments.parse_arguments() config = load_config() + logging.debug_mode = args.debug urls = get_urls(args) for url in urls: - source: Source = load_source(url) - if not source.authenticated and source.requires_authentication: - await authenticate(source, config, args) - result = await source.download(url) - if isinstance(result, Book): - with logging.progress(result.metadata.title, source.name) as progress: - template: str = args.output or "{title}.{ext}" - await download_with_progress(result, progress, template) - elif isinstance(result, Series): - with logging.progress(result.title, source.name, len(result.book_ids)) as progress: - for book_id in result.book_ids: - book: Book = await source.download_book_from_id(book_id) - template: str = args.output or "{series}/{title}.{ext}" - await download_with_progress(book, progress, template) - logging.info("") + try: + source: Source = load_source(url) + if not source.authenticated and source.requires_authentication: + await authenticate(source, config, args) + result = await source.download(url) + if isinstance(result, Book): + with logging.progress(result.metadata.title, source.name) as progress: + template: str = args.output or "{title}.{ext}" + await download_with_progress(result, progress, template) + elif isinstance(result, Series): + with logging.progress(result.title, source.name, len(result.book_ids)) as progress: + for book_id in result.book_ids: + book: Book = await source.download_book_from_id(book_id) + template: str = args.output or "{series}/{title}.{ext}" + await download_with_progress(book, progress, template) + logging.info("") + except GrawlixError as error: + error.print_error() async def download_with_progress(book: Book, progress: Progress, template: str): diff --git a/grawlix/arguments.py b/grawlix/arguments.py index 9c145bc..50237b0 100644 --- a/grawlix/arguments.py +++ b/grawlix/arguments.py @@ -51,4 +51,10 @@ def parse_arguments() -> argparse.Namespace: help = "Output destination", dest = "output" ) + # Logging + parser.add_argument( + '--debug', + help = "Enable debug messages", + dest = "debug" + ) return parser.parse_args() diff --git a/grawlix/assets/errors/data_not_found.txt b/grawlix/assets/errors/data_not_found.txt new file mode 100644 index 0000000..72664b6 --- /dev/null +++ b/grawlix/assets/errors/data_not_found.txt @@ -0,0 +1,9 @@ +[red]ERROR: Missing data from source[/red] + +grawlix is missing data from source. This can happen when you try to +download ebooks you don't have access too (not logged in or your profile +doesn't have the right permissions), the site has been updated and grawlix +hasn't implemented the new version, or it could be a bug in grawlix. + +If you think it is a problem with grawlix please create an issue at +https://github.com/jo1gi/grawlix/issues diff --git a/grawlix/assets/errors/invalid_url.txt b/grawlix/assets/errors/invalid_url.txt new file mode 100644 index 0000000..befd870 --- /dev/null +++ b/grawlix/assets/errors/invalid_url.txt @@ -0,0 +1,6 @@ +[red]ERROR: The given url does not match any sources[/red] + +Please make sure the link goes to the ebook you want to download and the site is +supported by grawlix. Try the link to the reader page if available. + +If nothing works please create an issue at {issue} diff --git a/grawlix/assets/errors/source_not_authenticated.txt b/grawlix/assets/errors/source_not_authenticated.txt new file mode 100644 index 0000000..efc44b6 --- /dev/null +++ b/grawlix/assets/errors/source_not_authenticated.txt @@ -0,0 +1,8 @@ +[red]ERROR: Source has not been authenticated[/red] + +You need to login to use this site. You can either use +* [green]--username[/] and [green]--password[/] +* Write username and password in the config file +* Use a cookie file with [green]--cookies[/] + +See the README for more information: {repo} diff --git a/grawlix/assets/errors/throttle.txt b/grawlix/assets/errors/throttle.txt new file mode 100644 index 0000000..c1db7cf --- /dev/null +++ b/grawlix/assets/errors/throttle.txt @@ -0,0 +1,4 @@ +[red]ERROR: Your connection has been throttled[/] + +The site you are downloading from has temporarily blocked you. +Please wait a bit before downloading again or try another site or account. diff --git a/grawlix/exceptions.py b/grawlix/exceptions.py index ece0c69..1832740 100644 --- a/grawlix/exceptions.py +++ b/grawlix/exceptions.py @@ -1,23 +1,30 @@ +from grawlix.logging import print_error_file + +ISSUE_URL = "https://github.com/jo1gi/grawlix/issues" +REPO_URL = "https://github.com/jo1gi/grawlix" + class GrawlixError(Exception): - pass + error_file: str + + def print_error(self) -> None: + print_error_file( + self.error_file, + repo = REPO_URL, + issue = ISSUE_URL, + ) + class DataNotFound(GrawlixError): - pass + error_file = "data_not_found" class InvalidUrl(GrawlixError): - pass + error_file = "invalid_url" class UnsupportedOutputFormat(GrawlixError): - pass - -class NoSourceFound(GrawlixError): - pass + error_file = "unsupported_output_format" class SourceNotAuthenticated(GrawlixError): - pass - -class MissingArgument(GrawlixError): - pass + error_file = "source_not_authenticated" class ThrottleError(GrawlixError): - pass + error_file = "throttle" diff --git a/grawlix/logging.py b/grawlix/logging.py index 5ac22d7..b15ab5b 100644 --- a/grawlix/logging.py +++ b/grawlix/logging.py @@ -1,22 +1,63 @@ from grawlix.book import Book -from rich.console import Console -from rich.progress import Progress, BarColumn, ProgressColumn, TaskID, SpinnerColumn import rich +from rich.console import Console +from rich.markup import render +from rich.progress import Progress, BarColumn, ProgressColumn, TaskID, SpinnerColumn +from rich.style import Style from typing import Union from dataclasses import dataclass +import importlib.resources console = Console(stderr=True) +debug_mode = False +DEBUG_PREFIX = render("[yellow bold]DEBUG[/]") + + +def debug(msg: str, remove_styling=False) -> None: + """ + Print debug message in console + + :param msg: Message to print + :param remove_styling: Remove automated styling from message + """ + if debug_mode: + if remove_styling: + rendered_msg = render(msg, style=Style(bold=False, color="white")) + console.print(DEBUG_PREFIX, rendered_msg) + else: + console.print(DEBUG_PREFIX, msg) + + def info(msg: str) -> None: """ - Print message in log + Print message in console :param msg: Message to print """ console.print(msg) + +def error(msg: str) -> None: + console.print(msg) + + +def print_error_file(name: str, **kwargs) -> None: + """ + Print predefined error message + + :param name: Name of error file + """ + message = importlib.resources.files("grawlix") \ + .joinpath(f"assets/errors/{name}.txt") \ + .read_text("utf8") \ + .format(**kwargs) \ + .strip() + error(message) + + def progress(category_name: str, source_name: str, count=1) -> Progress: if count > 1: console.print(f"Downloading [yellow not bold]{count}[/] books in [blue]{category_name}[/] from [magenta]{source_name}[/]") @@ -31,6 +72,7 @@ def progress(category_name: str, source_name: str, count=1) -> Progress: ) return progress + def add_book(progress: Progress, book: Book) -> TaskID: task = progress.add_task( f"[blue]{book.metadata.title}[/]", diff --git a/grawlix/sources/__init__.py b/grawlix/sources/__init__.py index 7e1fbf4..e718f7b 100644 --- a/grawlix/sources/__init__.py +++ b/grawlix/sources/__init__.py @@ -1,4 +1,4 @@ -from grawlix.exceptions import NoSourceFound +from grawlix.exceptions import InvalidUrl from .source import Source from .ereolen import Ereolen @@ -42,7 +42,7 @@ def find_source(url: str) -> type[Source]: for num, match in enumerate(cls.match): if re.match(match, url): return cls - raise NoSourceFound + raise InvalidUrl def get_source_classes() -> list[type[Source]]: diff --git a/grawlix/utils/__init__.py b/grawlix/utils/__init__.py index 613f59b..46471cd 100644 --- a/grawlix/utils/__init__.py +++ b/grawlix/utils/__init__.py @@ -2,6 +2,7 @@ from grawlix.exceptions import DataNotFound from urllib.parse import urlparse, parse_qs from functools import lru_cache +import importlib.resources def get_arg_from_url(url: str, key: str) -> str: parsed_url = urlparse(url) @@ -38,3 +39,14 @@ def nearest_string(input: str, list: list[str]) -> str: Finds the nearest string in `list` to `input` based on levenstein distance """ return sorted(list, key = lambda x: levenstein_distance(input, x))[0] + + +def read_asset_file(path: str) -> str: + """ + Read asset file from the grawlix module + + :param path: Path relative to root of grawlix module + """ + return importlib.resources.files("grawlix") \ + .joinpath(path) \ + .read_text(encoding="utf8")