From 80c8ca2b7b2cabc2d839716090160ad7fcc268b6 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 17 Nov 2024 01:41:41 -0500 Subject: [PATCH 1/3] refactor: move TUI user input options to a class 1. Redefine all TUI prompts into 3 levels: - `basic_prompt.py` for standard prompts like `ask_file_path`, `enter_to_continue` or `ask_text` - `user_prompts.py` for CDL specific prompts like `import v4 config` or `extract cookies from browser`. These prompts could have nested prompts - `program_ui.py` with the class `ProgramUI` which contains all the custom user prompts of the program as methods 2. All prompts are now dynamically created passing a dict with the menu options and a mapping with the function that each option will execute 3. Remove all prompts that were unnecessary cause they were asking the same values as the config files. `Edit URLs.txt`, `Edit current config`, `Edit authentication config` and `Edit global config` are still present in the UI but now they just open the respective file in notepad or the default text editor of the OS 4. Switch back to using `os.system('cls')` to clear the screen on Windows. `rich.console.clear()` uses ANSI escape codes to clear the terminal and the legacy `cmd` does not support them. This let to prompts being rendered over leftover characters in the buffer. 5. `Browser_Cookies` options `browsers` and `sites` are now defined as lists in the config definition 6. Add `all` and `default` as hardcoded internal configs names. The user cannot create configs with those names. The idea with reserving `all` is for the user to pass it as a CLI argument to indicate that they want to run all configs sequentially. This will help to collapse some CLI arguments which are essentially duplicated cause they have a `single config` version and an `all config` version. None of the logic for this is implemented in this PR, they were just added as a reserved name --- cyberdrop_dl/clients/hash_client.py | 4 +- cyberdrop_dl/main.py | 12 +- cyberdrop_dl/managers/manager.py | 3 +- cyberdrop_dl/scraper/scraper.py | 4 +- cyberdrop_dl/ui/program_ui.py | 302 +++++++++ cyberdrop_dl/ui/prompts/basic_prompts.py | 103 +++ cyberdrop_dl/ui/prompts/continue_prompt.py | 7 - cyberdrop_dl/ui/prompts/defaults.py | 7 + cyberdrop_dl/ui/prompts/general_prompts.py | 173 ----- .../settings_authentication_prompts.py | 294 --------- .../ui/prompts/settings_global_prompts.py | 278 -------- .../ui/prompts/settings_hash_prompts.py | 19 - .../ui/prompts/settings_user_prompts.py | 600 ------------------ cyberdrop_dl/ui/prompts/url_file_prompts.py | 30 - cyberdrop_dl/ui/prompts/user_prompts.py | 218 +++++++ cyberdrop_dl/ui/ui.py | 243 ------- .../utils/args/browser_cookie_extraction.py | 134 ---- cyberdrop_dl/utils/args/config_definitions.py | 4 +- cyberdrop_dl/utils/constants.py | 13 +- cyberdrop_dl/utils/cookie_extraction.py | 94 +++ cyberdrop_dl/utils/logger.py | 5 +- cyberdrop_dl/utils/sorting.py | 6 +- .../utils/transfer/transfer_v4_config.py | 2 +- cyberdrop_dl/utils/utilities.py | 57 +- 24 files changed, 810 insertions(+), 1802 deletions(-) create mode 100644 cyberdrop_dl/ui/program_ui.py create mode 100644 cyberdrop_dl/ui/prompts/basic_prompts.py delete mode 100644 cyberdrop_dl/ui/prompts/continue_prompt.py create mode 100644 cyberdrop_dl/ui/prompts/defaults.py delete mode 100644 cyberdrop_dl/ui/prompts/general_prompts.py delete mode 100644 cyberdrop_dl/ui/prompts/settings_authentication_prompts.py delete mode 100644 cyberdrop_dl/ui/prompts/settings_global_prompts.py delete mode 100644 cyberdrop_dl/ui/prompts/settings_hash_prompts.py delete mode 100644 cyberdrop_dl/ui/prompts/settings_user_prompts.py delete mode 100644 cyberdrop_dl/ui/prompts/url_file_prompts.py create mode 100644 cyberdrop_dl/ui/prompts/user_prompts.py delete mode 100644 cyberdrop_dl/ui/ui.py delete mode 100644 cyberdrop_dl/utils/args/browser_cookie_extraction.py create mode 100644 cyberdrop_dl/utils/cookie_extraction.py diff --git a/cyberdrop_dl/clients/hash_client.py b/cyberdrop_dl/clients/hash_client.py index 85527e040..25c17e1e8 100644 --- a/cyberdrop_dl/clients/hash_client.py +++ b/cyberdrop_dl/clients/hash_client.py @@ -9,7 +9,7 @@ from send2trash import send2trash -from cyberdrop_dl.ui.prompts.continue_prompt import enter_to_continue +from cyberdrop_dl.ui.prompts.basic_prompts import enter_to_continue from cyberdrop_dl.utils.logger import log if TYPE_CHECKING: @@ -53,7 +53,7 @@ async def startup(self) -> None: async def hash_directory(self, path: Path) -> None: path = Path(path) - async with self.manager.live_manager.get_hash_live(stop=True): + with self.manager.live_manager.get_hash_live(stop=True): if not path.is_dir(): raise NotADirectoryError for file in path.rglob("*"): diff --git a/cyberdrop_dl/main.py b/cyberdrop_dl/main.py index 6687ac6e0..28d2cf413 100644 --- a/cyberdrop_dl/main.py +++ b/cyberdrop_dl/main.py @@ -17,8 +17,8 @@ from cyberdrop_dl.clients.errors import InvalidYamlError from cyberdrop_dl.managers.manager import Manager from cyberdrop_dl.scraper.scraper import ScrapeMapper -from cyberdrop_dl.ui.ui import program_ui -from cyberdrop_dl.utils.args.browser_cookie_extraction import get_cookies_from_browser +from cyberdrop_dl.ui.program_ui import ProgramUI +from cyberdrop_dl.ui.prompts.user_prompts import get_cookies_from_browsers from cyberdrop_dl.utils.logger import ( log, log_spacer, @@ -48,7 +48,7 @@ def startup() -> Manager: manager.startup() if not manager.args_manager.immediate_download: - program_ui(manager) + ProgramUI(manager) except InvalidYamlError as e: print_to_console(e.message_rich) @@ -77,12 +77,12 @@ async def runtime(manager: Manager) -> None: def pre_runtime(manager: Manager) -> None: """Actions to complete before main runtime.""" if manager.config_manager.settings_data["Browser_Cookies"]["auto_import"]: - get_cookies_from_browser(manager) + get_cookies_from_browsers(manager) async def post_runtime(manager: Manager) -> None: """Actions to complete after main runtime, and before ui shutdown.""" - log_spacer(20) + log_spacer(20, log_to_console=False) log_with_color( f"Running Post-Download Processes For Config: {manager.config_manager.loaded_config}", "green", @@ -172,7 +172,7 @@ def setup_logger(manager: Manager, config_name: str) -> None: def ui_error_handling_wrapper(func: Callable) -> None: - """Wrapper handles errors from the main UI""" + """Wrapper handles errors from the main UI.""" @wraps(func) async def wrapper(*args, **kwargs): diff --git a/cyberdrop_dl/managers/manager.py b/cyberdrop_dl/managers/manager.py index f0203cb66..b5a741101 100644 --- a/cyberdrop_dl/managers/manager.py +++ b/cyberdrop_dl/managers/manager.py @@ -204,7 +204,8 @@ def args_logging(self) -> None: async def close(self) -> None: """Closes the manager.""" await self.db_manager.close() - await self.client_manager.close() + if not isinstance(self.client_manager, field): + await self.client_manager.close() self.db_manager: DBManager = field(init=False) self.cache_manager: CacheManager = field(init=False) self.hash_manager: HashManager = field(init=False) diff --git a/cyberdrop_dl/scraper/scraper.py b/cyberdrop_dl/scraper/scraper.py index 5b05dac61..b36de256d 100644 --- a/cyberdrop_dl/scraper/scraper.py +++ b/cyberdrop_dl/scraper/scraper.py @@ -21,7 +21,7 @@ remove_trailing_slash, ) from cyberdrop_dl.scraper.jdownloader import JDownloader -from cyberdrop_dl.utils.constants import BLOCKED_DOMAINS, PRELEASE_TAGS, REGEX_LINKS +from cyberdrop_dl.utils.constants import BLOCKED_DOMAINS, PRERELEASE_TAGS, REGEX_LINKS from cyberdrop_dl.utils.dataclasses.url_objects import MediaItem, ScrapeItem from cyberdrop_dl.utils.logger import log from cyberdrop_dl.utils.utilities import get_download_path, get_filename_and_ext @@ -79,7 +79,7 @@ def __init__(self, manager: Manager) -> None: "xxxbunker": self.xxxbunker, } - is_testing = next((tag for tag in PRELEASE_TAGS if tag in current_version), False) + is_testing = next((tag for tag in PRERELEASE_TAGS if tag in current_version), False) if is_testing: self.mapping["simpcity"] = self.simpcity diff --git a/cyberdrop_dl/ui/program_ui.py b/cyberdrop_dl/ui/program_ui.py new file mode 100644 index 000000000..7b2d0bdca --- /dev/null +++ b/cyberdrop_dl/ui/program_ui.py @@ -0,0 +1,302 @@ +from __future__ import annotations + +import sys +from functools import wraps +from textwrap import dedent +from typing import TYPE_CHECKING, Any + +from requests import request +from rich.console import Console +from rich.markdown import Markdown +from rich.text import Text + +from cyberdrop_dl.clients.hash_client import hash_directory_scanner +from cyberdrop_dl.dependencies import browser_cookie3 +from cyberdrop_dl.ui.prompts import user_prompts +from cyberdrop_dl.ui.prompts.basic_prompts import ask_dir_path, enter_to_continue +from cyberdrop_dl.ui.prompts.defaults import DONE_CHOICE, EXIT_CHOICE +from cyberdrop_dl.utils.transfer.transfer_v4_config import transfer_v4_config +from cyberdrop_dl.utils.transfer.transfer_v4_db import transfer_v4_db +from cyberdrop_dl.utils.utilities import check_latest_pypi, clear_term, open_in_text_editor + +if TYPE_CHECKING: + from pathlib import Path + + from InquirerPy.base.control import Choice + + from cyberdrop_dl.managers.manager import Manager + + +console = Console() +ERROR_PREFIX = Text("ERROR: ", style="bold red") + + +def repeat_until_done(func): + @wraps(func) + def wrapper(*args, **kwargs): + done = False + while not done: + done = func(*args, **kwargs) + return done + + return wrapper + + +class ProgramUI: + def __init__(self, manager: Manager, run: bool = True) -> None: + self.manager = manager + if run: + self.run() + + @staticmethod + def print_error(msg: str, critical: bool = False) -> None: + text = ERROR_PREFIX + msg + console.print(text, style="bold red" if critical else None) + if critical: + sys.exit(1) + enter_to_continue() + + @repeat_until_done + def run(self) -> None: + """Program UI.""" + clear_term() + options_map = { + -1: self._show_simpcity_disclaimer, + 1: self._download, + 2: self._retry_failed_download, + 3: self._scan_and_create_hashes, + 4: self._place_holder, + 5: self._edit_urls, + 6: self._change_config, + 7: self._manage_configs, + 8: self._import_from_v4, + 9: self._check_updates, + 10: self._view_changelog, + } + + answer = user_prompts.main_prompt(self.manager) + result = self._process_answer(answer, options_map) + return result and result != DONE_CHOICE + + def _download(self) -> True: + """Starts download process.""" + return True + + def _retry_failed_download(self) -> True: + """Sets retry failed and starts download process.""" + self.manager.args_manager.retry_failed = True + return True + + def _scan_and_create_hashes(self) -> None: + """Scans a folder and creates hashes for all of its files.""" + path = ask_dir_path("Select the directory to scan") + hash_directory_scanner(self.manager, path) + + def _check_updates(self) -> None: + """Checks Cyberdrop-DL updates.""" + check_latest_pypi(call_from_ui=True) + enter_to_continue() + + @repeat_until_done + def _import_from_v4(self) -> None: + options_map = { + 1: self._import_v4_config, + 2: self._import_v4_download_history, + } + answer = user_prompts.import_cyberdrop_v4_items_prompt(self.manager) + return self._process_answer(answer, options_map) + + def _import_v4_config(self) -> None: + new_config = user_prompts.import_v4_config_prompt(self.manager) + if not new_config: + return + transfer_v4_config(self.manager, *new_config) + + def _import_v4_download_history(self) -> None: + import_download_history_path = user_prompts.import_v4_download_history_prompt() + if import_download_history_path.is_file(): + transfer_v4_db(import_download_history_path, self.manager.path_manager.history_db) + return + + for item in import_download_history_path.glob("**/*.sqlite"): + if str(item) == str(self.manager.path_manager.history_db): + continue + try: + transfer_v4_db(item, self.manager.path_manager.history_db) + except Exception as e: + self.print_error(f"Unable to import {item.name}: {e!s}") + + def _change_config(self) -> None: + configs = self.manager.config_manager.get_configs() + selected_config = user_prompts.select_config(configs) + self.manager.config_manager.change_config(selected_config) + + def _view_changelog(self) -> None: + clear_term() + changelog_content = self._get_changelog() + if not changelog_content: + return + with console.pager(links=True): + console.print(Markdown(changelog_content, justify="left")) + + @repeat_until_done + def _manage_configs(self) -> None: + options_map = { + 1: self._change_default_config, + 2: self._create_new_config, + 3: self._delete_config, + 4: self._delete_cached_responses, + 5: self._edit_config, + 6: self._edit_auth_config, + 7: self._edit_global_config, + 8: self._edit_auto_cookies_extration, + 9: self._import_cookies_now, + } + answer = user_prompts.manage_configs(self.manager) + return self._process_answer(answer, options_map) + + def _delete_cached_responses(self) -> None: + self.print_error("function reserved for future version") + + def _edit_auth_config(self) -> None: + config_file = self.manager.path_manager.config_dir / "authentication.yaml" + self._open_in_text_editor(config_file) + + def _edit_global_config(self) -> None: + config_file = self.manager.path_manager.config_dir / "global_settings.yaml" + self._open_in_text_editor(config_file) + + def _edit_config(self) -> None: + config_file = self.manager.path_manager.config_dir / self.manager.config_manager.loaded_config / "settings.yaml" + self._open_in_text_editor(config_file) + + def _create_new_config(self) -> None: + config_name = user_prompts.create_new_config(self.manager) + if not config_name: + return + self.manager.config_manager.change_config(config_name) + config_file = self.manager.path_manager.config_dir / config_name / "settings.yaml" + self._open_in_text_editor(config_file) + + def _edit_urls(self) -> None: + config_file = self.manager.path_manager.config_dir / self.manager.config_manager.loaded_config / "URLs.txt" + self._open_in_text_editor(config_file) + + def _change_default_config(self) -> None: + configs = self.manager.config_manager.get_configs() + selected_config = user_prompts.select_config(configs) + self.manager.config_manager.change_default_config(selected_config) + + def _delete_config(self) -> None: + configs = self.manager.config_manager.get_configs() + if len(configs) == 1: + self.print_error("There is only one config") + return + + selected_config = user_prompts.select_config(configs) + if selected_config == self.manager.config_manager.loaded_config: + self.print_error("You cannot delete the currently active config") + return + + if self.manager.cache_manager.get("default_config") == selected_config: + self.print_error("You cannot delete the default config") + return + + self.manager.config_manager.delete_config(selected_config) + + def _edit_auto_cookies_extration(self) -> None: + user_prompts.auto_cookie_extraction(self.manager) + + def _import_cookies_now(self) -> None: + try: + user_prompts.extract_cookies(self.manager) + except browser_cookie3.BrowserCookieError as e: + self.print_error(str(e)) + + def _place_holder(self) -> None: + self.print_error("Option temporarily disabled on this version") + + def _show_simpcity_disclaimer(self) -> None: + simp_disclaimer = dedent(SIMPCITY_DISCLAIMER) + clear_term() + console.print(simp_disclaimer) + enter_to_continue() + + self.manager.cache_manager.save("simp_disclaimer_shown", True) + + def _open_in_text_editor(self, file_path: Path): + try: + open_in_text_editor(file_path) + except ValueError: + self.print_error("No default text editor found") + + def _process_answer(self, answer: Any, options_map=dict) -> Choice | None: + """Checks prompt answer and executes corresponding function.""" + if answer == EXIT_CHOICE.value: + sys.exit(0) + if answer == DONE_CHOICE.value: + return DONE_CHOICE + + function_to_call = options_map.get(answer) + if not function_to_call: + self.print_error("Something went wrong. Please report it to the developer", critical=True) + sys.exit(1) + + return function_to_call() + + def _get_changelog(self) -> str: + """Get latest changelog file from github. Returns its content.""" + path = self.manager.path_manager.config_dir.parent / "CHANGELOG.md" + url = "https://raw.githubusercontent.com/jbsparrow/CyberDropDownloader/refs/heads/master/CHANGELOG.md" + _, latest_version = check_latest_pypi(log_to_console=False) + name = f"{path.stem}_{latest_version}{path.suffix}" + changelog = path.with_name(name) + if not changelog.is_file(): + changelog_pattern = f"{path.stem}*{path.suffix}" + for old_changelog in path.parent.glob(changelog_pattern): + old_changelog.unlink() + try: + with request("GET", url, timeout=15) as response: + response.raise_for_status() + with changelog.open("wb") as f: + f.write(response.content) + except Exception: + self.print_error("UNABLE TO GET CHANGELOG INFORMATION") + return None + + lines = changelog.read_text(encoding="utf8").splitlines() + # remove keep_a_changelog disclaimer + return "\n".join(lines[:4] + lines[6:]) + + +SIMPCITY_DISCLAIMER = """ +\t\t[bold red]!! DISCLAIMER !![/bold red] + + +Due to the recent DDOS attacks on [italic]SimpCity[/italic], I have made some changes to [italic]Cyberdrop-DL[/italic]. + +First and foremost, we have removed support for scraping [italic]SimpCity[/italic] for the time being. I know that this will upset many of you, but hopefully, you will understand my reasoning. + + +Because of the DDOS attacks that [italic]SimpCity[/italic] has been receiving, they have been forced to implement some protective features such as using a DDOS-Guard browser check, only allowing [link=https://simpcity.su/threads/emails-august-2024.365869/]whitelisted email domains[/link] to access the website, and [link=https://simpcity.su/threads/rate-limit-429-error.397746/]new rate limits[/link]. +[italic]Cyberdrop-DL[/italic] allows a user to scrape a model's entire thread in seconds, downloading all the files that it finds. This is great but can be problematic for a few reasons: +\t- We end up downloading a lot of content that we will never view. +\t- Such large-scale scraping with no limits puts a large strain on [italic]SimpCity[/italic]'s servers, especially when they are getting DDOSed. +\t- Scraping has no benefit for [italic]SimpCity[/italic] - they gain nothing from us scraping their website. + +For those reasons, [italic]SimpCity[/italic] has decided that they don't want to allow automated thread scraping anymore, and have removed the [italic]Cyberdrop-DL[/italic] thread from their website. +I want to respect [italic]SimpCity[/italic]'s wishes, and as a result, have disabled scraping for [italic]SimpCity[/italic] links. + +In order to help reduce the impact that [italic]Cyberdrop-DL[/italic] has on other websites, I have decided to enable the [italic bold]update_last_forum_post[/italic bold] setting for all users' configs. +You can disable it again after reading through this disclaimer, however, I would recommend against it. [italic bold]update_last_forum_post[/italic bold] actually speeds up scrapes and reduces the load on websites' servers by not re-scraping entire threads and picking up where it left off last time. + +Furthermore, I have adjusted the default rate-limiting settings in an effort to reduce the impact that [italic]Cyberdrop-DL[/italic] will have on websites. + +I encourage you to be conscientious about how you use [italic]Cyberdrop-DL[/italic]. +Some tips on how to reduce the impact your use of [italic]Cyberdrop-DL[/italic] will have on a website: +\t- Try to avoid looping runs repeatedly. +\t- If you have a large URLs file, try to comb through it occasionally and get rid of items you don't want anymore, and try to run [italic]Cyberdrop-DL[/italic] less often. +\t- Avoid downloading content you don't want. It's good to scan through the content quickly to ensure it's not a bunch of stuff you're going to delete after downloading it. + +If you do want to continue downloading from SimpCity, you can use a tampermonkey script like this one: [link=https://simpcity.su/threads/forum-post-downloader-tampermonkey-script.96714/]SimpCity Tampermonkey Forum Downloader[/link] +""" diff --git a/cyberdrop_dl/ui/prompts/basic_prompts.py b/cyberdrop_dl/ui/prompts/basic_prompts.py new file mode 100644 index 000000000..f97be86f8 --- /dev/null +++ b/cyberdrop_dl/ui/prompts/basic_prompts.py @@ -0,0 +1,103 @@ +from pathlib import Path + +from InquirerPy import inquirer +from InquirerPy.base.control import Choice +from InquirerPy.separator import Separator +from InquirerPy.validator import EmptyInputValidator, PathValidator + +from cyberdrop_dl.ui.prompts.defaults import DEFAULT_OPTIONS, DONE_CHOICE + + +def ask_text(message: str, validate_empty: bool = True, **kwargs): + options = DEFAULT_OPTIONS | kwargs + return inquirer.text( + message=message, + validate=EmptyInputValidator("Input should not be empty") if validate_empty else None, + **options, + ).execute() + + +def ask_choice(choices: list[Choice], *, message: str = "What would you like to do:", **kwargs): + options = DEFAULT_OPTIONS | kwargs + return inquirer.select(message=message, choices=choices, **options).execute() + + +def ask_multi_choice(choices: list[Choice], *, message: str = "What would you like to do:", **kwargs): + return ask_choice(choices, message=message, multiselect=True, **kwargs) + + +def ask_checkbox(choices: list[Choice], *, message: str = "Select multiple options:", **kwargs): + options = DEFAULT_OPTIONS | {"long_instruction": "ARROW KEYS: Navigate | SPACE: Select | ENTER: Confirm"} | kwargs + return inquirer.checkbox(message=message, choices=choices, **options).execute() + + +def ask_choice_fuzzy(message: str, choices: list[Choice], validate_empty: bool = True, **kwargs): + options = ( + DEFAULT_OPTIONS + | {"long_instruction": "ARROW KEYS: Navigate | TYPE: Filter | TAB: select, ENTER: Finish Selection"} + | kwargs + ) + return inquirer.fuzzy( + message=message, + choices=choices, + validate=EmptyInputValidator("Input should not be empty") if validate_empty else None, + **options, + ).execute() + + +def ask_path(message: str = "Select path", *, validator_options: dict | None = None, **kwargs) -> Path: + options = DEFAULT_OPTIONS | kwargs + return Path( + inquirer.filepath( + message=message, default=str(Path.home()), validate=PathValidator(**(validator_options or {})), **options + ).execute() + ) + + +def ask_file_path(message: str = "Select file path", **kwargs): + options = DEFAULT_OPTIONS | kwargs + validator_options = {"is_file": True, "message": "Input is not a file"} + return ask_path(message, validator_options=validator_options, **options) + + +def ask_dir_path(message: str = "Select dir path", **kwargs): + options = DEFAULT_OPTIONS | kwargs + validator_options = {"is_dir": True, "message": "Input is not a directory"} + return ask_path(message, validator_options=validator_options, **options) + + +def ask_toggle(message: str = "enable", **kwargs): + options = DEFAULT_OPTIONS | {"long_instruction": "Y: Yes | N: No"} | kwargs + return inquirer.confirm(message=message, **options).execute() + + +def enter_to_continue(message: str = "Press to continue", **kwargs): + options = DEFAULT_OPTIONS | {"long_instruction": "ENTER: continue"} | kwargs + msg = f"\n{message}" + return inquirer.confirm(message=msg, qmark="", **options).execute() + + +def create_choices( + options_groups: list[list[str]] | dict[str, list[list[str]]], + append_last: Choice = DONE_CHOICE, + *, + disabled_choices: list[str] | None = None, +): + if isinstance(options_groups, dict): + options_groups = list(options_groups.values()) + disabled_choices = disabled_choices or [] + options = [option for group in options_groups for option in group] + choices = [] + for index, option in enumerate(options, 1): + enabled = option not in disabled_choices + choices.append(Choice(index, option, enabled)) + choices.append(append_last) + + separator_indexes = [] + for group in options_groups[:-1]: + separator_indexes.append(len(group) + (separator_indexes[-1] if separator_indexes else 0)) + + for count, index in enumerate(separator_indexes): + choices.insert(index + count, Separator()) + + return choices diff --git a/cyberdrop_dl/ui/prompts/continue_prompt.py b/cyberdrop_dl/ui/prompts/continue_prompt.py deleted file mode 100644 index 3e11b74d1..000000000 --- a/cyberdrop_dl/ui/prompts/continue_prompt.py +++ /dev/null @@ -1,7 +0,0 @@ -from InquirerPy import inquirer - - -def enter_to_continue() -> None: - inquirer.text( - message="press enter to continue", - ).execute() diff --git a/cyberdrop_dl/ui/prompts/defaults.py b/cyberdrop_dl/ui/prompts/defaults.py new file mode 100644 index 000000000..bc5e4b993 --- /dev/null +++ b/cyberdrop_dl/ui/prompts/defaults.py @@ -0,0 +1,7 @@ +from InquirerPy.base.control import Choice + +EXIT_CHOICE = Choice("Exit") +DONE_CHOICE = Choice("Done") +ALL_CHOICE = Choice("All of the above") + +DEFAULT_OPTIONS = {"long_instruction": "ARROW KEYS: Navigate | ENTER: Select", "vi_mode": False} diff --git a/cyberdrop_dl/ui/prompts/general_prompts.py b/cyberdrop_dl/ui/prompts/general_prompts.py deleted file mode 100644 index 85c110964..000000000 --- a/cyberdrop_dl/ui/prompts/general_prompts.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import os -import pathlib -from typing import TYPE_CHECKING - -from InquirerPy import get_style, inquirer -from InquirerPy.base.control import Choice -from InquirerPy.separator import Separator -from InquirerPy.validator import EmptyInputValidator, PathValidator -from rich.console import Console - -from cyberdrop_dl.utils.logger import log -from cyberdrop_dl.utils.transfer.transfer_v4_config import transfer_v4_config -from cyberdrop_dl.utils.transfer.transfer_v4_db import transfer_v4_db - -if TYPE_CHECKING: - from cyberdrop_dl.managers.manager import Manager - -console = Console() - - -def main_prompt(manager: Manager) -> int: - """Main prompt for the program.""" - choices = [ - Choice(1, "Download"), - Choice(2, "Download (All Configs)"), - Choice(3, "Retry Failed Downloads"), - Choice(4, "Create File Hashes"), - Choice(5, "Sort All Configs"), - Choice(6, "Edit URLs"), - Separator(), - Choice(7, f"Select Config (Current: {manager.config_manager.loaded_config})"), - Choice(8, "Change URLs.txt file and Download Location"), - Choice(9, "Edit Configs"), - Separator(), - Choice(10, "Check for Updates"), - Choice(11, "Import Cyberdrop_V4 Items"), - Choice(12, "View Changelog"), - Choice(13, "Exit"), - ] - - simp_disclaimer_shown = manager.cache_manager.get("simp_disclaimer_shown") - if not simp_disclaimer_shown: - choices = [Choice(-1, "!! VIEW DISCLAIMER !!")] - - return inquirer.select( - message="What would you like to do?", - choices=choices, - long_instruction="ARROW KEYS: Navigate | ENTER: Select", - style=get_style({"pointer": "#ff0000 bold"}) if not simp_disclaimer_shown else None, - vi_mode=manager.vi_mode, - ).execute() - - -def manage_configs_prompt(manager: Manager) -> int: - """Manage Configs Prompt.""" - console.clear() - return inquirer.select( - message="What would you like to do?", - choices=[ - Choice(1, "Change Default Config"), - Choice(2, "Create A New Config"), - Choice(3, "Delete A Config"), - Separator(), - Choice(4, "Edit Config"), - Choice(5, "Edit Authentication Values"), - Choice(6, "Edit Global Values"), - Choice(7, "Done"), - ], - long_instruction="ARROW KEYS: Navigate | ENTER: Select", - vi_mode=manager.vi_mode, - ).execute() - - -def select_config_prompt(manager: Manager, configs: list) -> str: - """Select a config file from a list of configs.""" - return inquirer.fuzzy( - choices=configs, - multiselect=False, - validate=lambda result: len(result) > 0, - invalid_message="Need to select a config.", - message="Select a config file:", - long_instruction="ARROW KEYS: Navigate | TYPE: Filter | TAB: select, ENTER: Finish Selection", - vi_mode=manager.vi_mode, - ).execute() - - -def import_cyberdrop_v4_items_prompt(manager: Manager) -> None: - """Import Cyberdrop_V4 Items.""" - while True: - console.clear() - console.print("Editing Config Values") - action = inquirer.select( - message="What would you like to do?", - choices=[ - Choice(1, "Import Config"), - Choice(2, "Import download_history.sql"), - Choice(3, "Done"), - ], - long_instruction="ARROW KEYS: Navigate | ENTER: Select", - vi_mode=manager.vi_mode, - ).execute() - - # Import Config - if action == 1: - new_config_name = inquirer.text( - message="What should this config be called?", - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - - if (manager.path_manager.config_dir / new_config_name).is_dir(): - console.print(f"Config with name '{new_config_name}' already exists!") - inquirer.confirm(message="Press enter to return to the import menu.").execute() - continue - - home_path = "~/" if os.name == "posix" else "C:\\" - import_config_path = inquirer.filepath( - message="Select the config file to import", - default=home_path, - validate=PathValidator(is_file=True, message="Input is not a file"), - ).execute() - - transfer_v4_config(manager, import_config_path, new_config_name) - - # Import download_history.sql - elif action == 2: - home_path = "~/" if os.name == "posix" else "C:\\" - import_download_history_path = inquirer.filepath( - message="Select the download_history.sql file to import", - default=home_path, - validate=PathValidator(message="Input is not a file"), - vi_mode=manager.vi_mode, - filter=lambda x: pathlib.Path(x), - ).execute() - if import_download_history_path.is_file(): - transfer_v4_db(import_download_history_path, manager.path_manager.history_db) - else: - for ele in pathlib.Path(import_download_history_path).glob("**/*.sqlite"): - if str(ele) == str(manager.path_manager.history_db): - continue - try: - transfer_v4_db(ele, manager.path_manager.history_db) - except Exception as e: - log(f"Error importing {ele.name}: {e!s}", 20) - - # Done - elif action == 3: - break - - -def donations_prompt(manager: Manager) -> None: - """Donations prompt.""" - console.clear() - console.print("[bold]Donations[/bold]") - console.print("") - console.print( - "I started making this program around three years ago at this point," - "\nIt has grown larger than I could've imagined and I'm very proud of it." - "\nI have put a lot of time and effort into this program and I'm glad that people are using it." - "\nThanks to everyone that have supported me, " - "it keeps me motivated to continue working on this program.", - ) - console.print("") - console.print("If you'd like to support me and my work, you can donate to me via the following methods:") - console.print("BuyMeACoffee: https://www.buymeacoffee.com/jbsparrow") - console.print("Github Sponsor: https://github.com/sponsors/jbsparrow") - - console.print("") - console.print("Thank you for your support!") - console.print("") - inquirer.confirm(message="Press enter to return to the main menu.", vi_mode=manager.vi_mode).execute() diff --git a/cyberdrop_dl/ui/prompts/settings_authentication_prompts.py b/cyberdrop_dl/ui/prompts/settings_authentication_prompts.py deleted file mode 100644 index f0d5cb92b..000000000 --- a/cyberdrop_dl/ui/prompts/settings_authentication_prompts.py +++ /dev/null @@ -1,294 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from InquirerPy import inquirer -from InquirerPy.base.control import Choice -from rich.console import Console - -from cyberdrop_dl.managers.manager import Manager -from cyberdrop_dl.utils.args.browser_cookie_extraction import get_cookies_from_browser - -if TYPE_CHECKING: - from cyberdrop_dl.managers.manager import Manager - -console = Console() - - -def edit_authentication_values_prompt(manager: Manager) -> None: - """Edit the authentication values.""" - auth = manager.config_manager.authentication_data - - while True: - console.clear() - console.print("Editing Authentication Values") - action = inquirer.select( - message="What would you like to do?", - choices=[ - Choice(1, "Edit Forum Authentication Values"), - Choice(2, "Edit JDownloader Authentication Values"), - Choice(3, "Edit Reddit Authentication Values"), - Choice(4, "Edit GoFile API Key"), - Choice(5, "Edit Imgur Client ID"), - Choice(6, "Edit PixelDrain API Key"), - Choice(7, "Done"), - ], - long_instruction="ARROW KEYS: Navigate | ENTER: Select", - vi_mode=manager.vi_mode, - ).execute() - - # Edit Forums - if action == 1: - edit_forum_authentication_values_prompt(manager) - - # Edit JDownloader - elif action == 2: - edit_jdownloader_authentication_values_prompt(auth) - - # Edit Reddit - elif action == 3: - edit_reddit_authentication_values_prompt(auth) - - # Edit GoFile API Key - elif action == 4: - console.clear() - gofile_api_key = inquirer.text( - message="Enter the GoFile API Key:", - default=auth["GoFile"]["gofile_api_key"], - long_instruction="You can get your premium GoFile API Key from https://gofile.io/myProfile", - vi_mode=manager.vi_mode, - ).execute() - auth["GoFile"]["gofile_api_key"] = gofile_api_key - - # Edit Imgur Client ID - elif action == 5: - console.clear() - imgur_client_id = inquirer.text( - message="Enter the Imgur Client ID:", - default=auth["Imgur"]["imgur_client_id"], - long_instruction="You can create an app and get your client ID " - "from https://imgur.com/account/settings/apps", - vi_mode=manager.vi_mode, - ).execute() - auth["Imgur"]["imgur_client_id"] = imgur_client_id - - # Edit PixelDrain API Key - elif action == 6: - console.clear() - pixeldrain_api_key = inquirer.text( - message="Enter the PixelDrain API Key:", - default=auth["PixelDrain"]["pixeldrain_api_key"], - long_instruction="You can get your premium API Key from https://pixeldrain.com/user/api_keys", - vi_mode=manager.vi_mode, - ).execute() - auth["PixelDrain"]["pixeldrain_api_key"] = pixeldrain_api_key - - # Done - elif action == 7: - manager.config_manager.write_updated_authentication_config() - return - - -def edit_forum_authentication_values_prompt(manager: Manager) -> None: - """Edit the forum authentication values.""" - while True: - console.clear() - console.print("Editing Forum Authentication Values") - action = inquirer.select( - message="What would you like to do?", - choices=[ - Choice(1, "Browser Cookie Extraction"), - Choice(2, "Enter Cookie Values Manually"), - Choice(3, "Done"), - ], - long_instruction="ARROW KEYS: Navigate | ENTER: Select", - vi_mode=manager.vi_mode, - ).execute() - - # Browser Cookie Extraction - if action == 1: - action = inquirer.select( - message="Which browser should we load cookies from?", - choices=[ - Choice("chrome", "Chrome"), - Choice("chromium", "Chromium"), - Choice("firefox", "FireFox"), - Choice("edge", "Edge"), - Choice("safari", "Safari"), - Choice("opera", "Opera"), - Choice("opera_gx", "Opera_GX"), - Choice("librewolf", "LibreWolf"), - Choice("vivaldi", "Vivaldi"), - Choice("brave", "Brave"), - Choice(1, "Done"), - ], - long_instruction="ARROW KEYS: Navigate | ENTER: Select", - vi_mode=manager.vi_mode, - ).execute() - - # Done - if action == 1: - continue - - # Browser Selection - if action == "chrome": - get_cookies_from_browser(manager, "chrome") - elif action == "firefox": - get_cookies_from_browser(manager, "firefox") - elif action == "edge": - get_cookies_from_browser(manager, "edge") - elif action == "safari": - get_cookies_from_browser(manager, "safari") - elif action == "opera": - get_cookies_from_browser(manager, "opera") - elif action == "brave": - get_cookies_from_browser(manager, "brave") - elif action == "libreWolf": - get_cookies_from_browser(manager, "librewolf") - elif action == "opera_gx": - get_cookies_from_browser(manager, "opera_gx") - elif action == "vivaldi": - get_cookies_from_browser(manager, "vivaldi") - elif action == "chromium": - get_cookies_from_browser(manager, "chromium") - return - - # Enter Cred Values Manually - if action == 2: - celebforum_username = inquirer.text( - message="Enter your CelebForum Username:", - default=manager.config_manager.authentication_data["Forums"]["celebforum_username"], - vi_mode=manager.vi_mode, - ).execute() - celebforum_password = inquirer.text( - message="Enter your CelebForum Password:", - default=manager.config_manager.authentication_data["Forums"]["celebforum_password"], - vi_mode=manager.vi_mode, - ).execute() - - f95zone_username = inquirer.text( - message="Enter your F95Zone Username:", - default=manager.config_manager.authentication_data["Forums"]["f95zone_username"], - vi_mode=manager.vi_mode, - ).execute() - f95zone_password = inquirer.text( - message="Enter your F95Zone Password:", - default=manager.config_manager.authentication_data["Forums"]["f95zone_password"], - vi_mode=manager.vi_mode, - ).execute() - - leakedmodels_username = inquirer.text( - message="Enter your LeakedModels Username:", - default=manager.config_manager.authentication_data["Forums"]["leakedmodels_username"], - vi_mode=manager.vi_mode, - ).execute() - leakedmodels_password = inquirer.text( - message="Enter your LeakedModels Password:", - default=manager.config_manager.authentication_data["Forums"]["leakedmodels_password"], - vi_mode=manager.vi_mode, - ).execute() - - nudostar_username = inquirer.text( - message="Enter your NudoStar Username:", - default=manager.config_manager.authentication_data["Forums"]["nudostar_username"], - vi_mode=manager.vi_mode, - ).execute() - nudostar_password = inquirer.text( - message="Enter your NudoStar Password:", - default=manager.config_manager.authentication_data["Forums"]["nudostar_password"], - vi_mode=manager.vi_mode, - ).execute() - - simpcity_username = inquirer.text( - message="Enter your SimpCity Username:", - default=manager.config_manager.authentication_data["Forums"]["simpcity_username"], - vi_mode=manager.vi_mode, - ).execute() - simpcity_password = inquirer.text( - message="Enter your SimpCity Password:", - default=manager.config_manager.authentication_data["Forums"]["simpcity_password"], - vi_mode=manager.vi_mode, - ).execute() - - socialmediagirls_username = inquirer.text( - message="Enter your SocialMediaGirls Username:", - default=manager.config_manager.authentication_data["Forums"]["socialmediagirls_username"], - vi_mode=manager.vi_mode, - ).execute() - socialmediagirls_password = inquirer.text( - message="Enter your SocialMediaGirls Password:", - default=manager.config_manager.authentication_data["Forums"]["socialmediagirls_password"], - vi_mode=manager.vi_mode, - ).execute() - - xbunker_username = inquirer.text( - message="Enter your XBunker Username:", - default=manager.config_manager.authentication_data["Forums"]["xbunker_username"], - vi_mode=manager.vi_mode, - ).execute() - xbunker_password = inquirer.text( - message="Enter your XBunker Password:", - default=manager.config_manager.authentication_data["Forums"]["xbunker_password"], - vi_mode=manager.vi_mode, - ).execute() - - manager.config_manager.authentication_data["Forums"]["celebforum_username"] = celebforum_username - manager.config_manager.authentication_data["Forums"]["f95zone_username"] = f95zone_username - manager.config_manager.authentication_data["Forums"]["leakedmodels_username"] = leakedmodels_username - manager.config_manager.authentication_data["Forums"]["nudostar_username"] = nudostar_username - manager.config_manager.authentication_data["Forums"]["simpcity_username"] = simpcity_username - manager.config_manager.authentication_data["Forums"]["socialmediagirls_username"] = ( - socialmediagirls_username - ) - manager.config_manager.authentication_data["Forums"]["xbunker_username"] = xbunker_username - - manager.config_manager.authentication_data["Forums"]["celebforum_password"] = celebforum_password - manager.config_manager.authentication_data["Forums"]["f95zone_password"] = f95zone_password - manager.config_manager.authentication_data["Forums"]["leakedmodels_password"] = leakedmodels_password - manager.config_manager.authentication_data["Forums"]["nudostar_password"] = nudostar_password - manager.config_manager.authentication_data["Forums"]["simpcity_password"] = simpcity_password - manager.config_manager.authentication_data["Forums"]["socialmediagirls_password"] = ( - socialmediagirls_password - ) - manager.config_manager.authentication_data["Forums"]["xbunker_password"] = xbunker_password - return - if action == 3: - return - - -def edit_jdownloader_authentication_values_prompt(auth: dict) -> None: - """Edit the JDownloader authentication values.""" - console.clear() - jdownloader_username = inquirer.text( - message="Enter the JDownloader Username:", - default=auth["JDownloader"]["jdownloader_username"], - ).execute() - jdownloader_password = inquirer.text( - message="Enter the JDownloader Password:", - default=auth["JDownloader"]["jdownloader_password"], - ).execute() - jdownloader_device = inquirer.text( - message="Enter the JDownloader Device Name:", - default=auth["JDownloader"]["jdownloader_device"], - ).execute() - - auth["JDownloader"]["jdownloader_username"] = jdownloader_username - auth["JDownloader"]["jdownloader_password"] = jdownloader_password - auth["JDownloader"]["jdownloader_device"] = jdownloader_device - - -def edit_reddit_authentication_values_prompt(auth: dict) -> None: - """Edit the reddit authentication values.""" - console.clear() - console.print("You can create a Reddit App to use here: https://www.reddit.com/prefs/apps/") - reddit_secret = inquirer.text( - message="Enter the Reddit Secret value:", - default=auth["Reddit"]["reddit_secret"], - ).execute() - reddit_personal_use_script = inquirer.text( - message="Enter the Reddit Personal Use Script value:", - default=auth["Reddit"]["reddit_personal_use_script"], - ).execute() - - auth["Reddit"]["reddit_secret"] = reddit_secret - auth["Reddit"]["reddit_personal_use_script"] = reddit_personal_use_script diff --git a/cyberdrop_dl/ui/prompts/settings_global_prompts.py b/cyberdrop_dl/ui/prompts/settings_global_prompts.py deleted file mode 100644 index 0a06c567d..000000000 --- a/cyberdrop_dl/ui/prompts/settings_global_prompts.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from InquirerPy import inquirer -from InquirerPy.base.control import Choice -from InquirerPy.validator import EmptyInputValidator -from rich.console import Console - -if TYPE_CHECKING: - from cyberdrop_dl.managers.manager import Manager - -console = Console() - - -def edit_global_settings_prompt(manager: Manager) -> None: - """Edit the authentication values.""" - while True: - console.clear() - console.print("Editing Global Settings") - action = inquirer.select( - message="What would you like to do?", - choices=[ - Choice(1, "Edit General Settings"), - Choice(2, "Edit Rate Limiting Settings"), - Choice(3, "Edit UI Options"), - Choice(4, "Edit Dupe Options"), - Choice(5, "Done"), - ], - vi_mode=manager.vi_mode, - ).execute() - - # Edit General Settings - if action == 1: - edit_general_settings_prompt(manager) - - # Edit Rate Limiting Settings - elif action == 2: - edit_rate_limiting_settings_prompt(manager) - - # Edit UI Settings - elif action == 3: - edit_ui_settings_prompt(manager) - - # Edit Dupe - elif action == 4: - edit_dupe_settings_prompt(manager) - - # Done - elif action == 5: - manager.config_manager.write_updated_global_settings_config() - break - - -def edit_general_settings_prompt(manager: Manager) -> None: - """Edit the general settings.""" - console.clear() - console.print("Editing General Settings") - allow_insecure_connections = inquirer.confirm("Allow insecure connections?", vi_mode=manager.vi_mode).execute() - user_agent = inquirer.text( - message="User Agent:", - default=manager.config_manager.global_settings_data["General"]["user_agent"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - proxy = inquirer.text( - message="Proxy:", - default=manager.config_manager.global_settings_data["General"]["proxy"], - vi_mode=manager.vi_mode, - ).execute() - flaresolverr = inquirer.text( - message="FlareSolverr (IP:PORT):", - default=manager.config_manager.global_settings_data["General"]["flaresolverr"], - vi_mode=manager.vi_mode, - ).execute() - max_filename_length = inquirer.number( - message="Max Filename Length:", - default=int(manager.config_manager.global_settings_data["General"]["max_file_name_length"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - max_folder_name_length = inquirer.number( - message="Max Folder Name Length:", - default=int(manager.config_manager.global_settings_data["General"]["max_folder_name_length"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - required_free_space = inquirer.number( - message="Required Free Space (in GB):", - default=int(manager.config_manager.global_settings_data["General"]["required_free_space"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - - manager.config_manager.global_settings_data["General"]["allow_insecure_connections"] = allow_insecure_connections - manager.config_manager.global_settings_data["General"]["user_agent"] = user_agent - manager.config_manager.global_settings_data["General"]["proxy"] = proxy - manager.config_manager.global_settings_data["General"]["flaresolverr"] = flaresolverr - manager.config_manager.global_settings_data["General"]["max_filename_length"] = int(max_filename_length) - manager.config_manager.global_settings_data["General"]["max_folder_name_length"] = int(max_folder_name_length) - manager.config_manager.global_settings_data["General"]["required_free_space"] = int(required_free_space) - - -def edit_ui_settings_prompt(manager: Manager) -> None: - """Edit the progress settings.""" - console.clear() - console.print("Editing UI Settings") - refresh_rate = inquirer.number( - message="Refresh Rate:", - default=int(manager.config_manager.global_settings_data["UI_Options"]["refresh_rate"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - scraping_item_limit = inquirer.number( - message="Number of lines to allow for scraping items before overflow:", - default=int(manager.config_manager.global_settings_data["UI_Options"]["scraping_item_limit"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - downloading_item_limit = inquirer.number( - message="Number of lines to allow for download items before overflow:", - default=int(manager.config_manager.global_settings_data["UI_Options"]["downloading_item_limit"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - - vi_mode = inquirer.confirm( - message="Enable VI/VIM keybindings?", - default=bool(manager.config_manager.global_settings_data["UI_Options"]["vi_mode"]), - vi_mode=manager.vi_mode, - ).execute() - - manager.config_manager.global_settings_data["UI_Options"]["refresh_rate"] = int(refresh_rate) - manager.config_manager.global_settings_data["UI_Options"]["scraping_item_limit"] = int(scraping_item_limit) - manager.config_manager.global_settings_data["UI_Options"]["downloading_item_limit"] = int(downloading_item_limit) - manager.config_manager.global_settings_data["UI_Options"]["vi_mode"] = vi_mode - manager.vi_mode = vi_mode - - -def edit_rate_limiting_settings_prompt(manager: Manager) -> None: - """Edit the rate limiting settings.""" - console.clear() - console.print("Editing Rate Limiting Settings") - connection_timeout = inquirer.number( - message="Connection Timeout (in seconds):", - default=int(manager.config_manager.global_settings_data["Rate_Limiting_Options"]["connection_timeout"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - read_timeout = inquirer.number( - message="Read Timeout (in seconds):", - default=int(manager.config_manager.global_settings_data["Rate_Limiting_Options"]["read_timeout"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - download_attempts = inquirer.number( - message="Download Attempts:", - default=int(manager.config_manager.global_settings_data["Rate_Limiting_Options"]["download_attempts"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - rate_limit = inquirer.number( - message="Maximum number of requests per second:", - default=int(manager.config_manager.global_settings_data["Rate_Limiting_Options"]["rate_limit"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - - download_speed_limit = inquirer.number( - message="Maximum download speed per second in kb", - default=int(manager.config_manager.global_settings_data["Rate_Limiting_Options"]["download_speed_limit"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - throttle = inquirer.number( - message="Delay between requests during the download stage:", - default=float(manager.config_manager.global_settings_data["Rate_Limiting_Options"]["download_delay"]), - float_allowed=True, - vi_mode=manager.vi_mode, - ).execute() - - max_simultaneous_downloads = inquirer.number( - message="Maximum number of simultaneous downloads:", - default=int(manager.config_manager.global_settings_data["Rate_Limiting_Options"]["max_simultaneous_downloads"]), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - max_simultaneous_downloads_per_domain = inquirer.number( - message="Maximum number of simultaneous downloads per domain:", - default=int( - manager.config_manager.global_settings_data["Rate_Limiting_Options"][ - "max_simultaneous_downloads_per_domain" - ], - ), - float_allowed=False, - vi_mode=manager.vi_mode, - ).execute() - - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["connection_timeout"] = int(connection_timeout) - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["read_timeout"] = int(read_timeout) - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["download_attempts"] = int(download_attempts) - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["rate_limit"] = int(rate_limit) - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["download_speed_limit"] = int( - download_speed_limit, - ) - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["download_delay"] = float(throttle) - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["max_simultaneous_downloads"] = int( - max_simultaneous_downloads, - ) - manager.config_manager.global_settings_data["Rate_Limiting_Options"]["max_simultaneous_downloads_per_domain"] = int( - max_simultaneous_downloads_per_domain, - ) - - -def edit_dupe_settings_prompt(manager: Manager) -> None: - """Edit the duplicate file removal settings.""" - console.clear() - console.print("Editing Duplicate File Settings") - - delete_after = inquirer.select( - message="Toggle duplicate files deletion using hashes:", - default=manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["delete_after_download"], - choices=[Choice(True, "True"), Choice(False, "False")], - vi_mode=manager.vi_mode, - ).execute() - hash_while_downloading = inquirer.select( - message="Hash Files during downloading:", - long_instruction=""" - Generate file hashes after each download, instead of batched - together during deduplication process - """, - default=manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["hash_while_downloading"], - choices=[Choice(True, "True"), Choice(False, "False")], - vi_mode=manager.vi_mode, - ).execute() - - dedupe_already_downloaded = inquirer.select( - message="How to handle files already on system: ", - default=manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["dedupe_already_downloaded"], - choices=[Choice(True, "Mark Existing Files as 'new'"), Choice(False, "Skip Existing Files")], - vi_mode=manager.vi_mode, - ).execute() - - keep_current = inquirer.select( - message="What to do with new file when deduping: ", - long_instruction="Keep a curent file. Current files are files that were either downloaded or a file was skipped for already existing when dedupe_already_downloaded is true", - default=manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["keep_new_download"], - choices=[Choice(True, "Keep a newfile"), Choice(False, "Delete all new files")], - vi_mode=manager.vi_mode, - ).execute() - - keep_prev = inquirer.select( - message="What to do with previous file(s) when deduping:", - default=manager.config_manager.global_settings_data["Dupe_Cleanup_Options"], - long_instruction="Any file that is not in the list of current files with a matching hash", - choices=[Choice(True, "Keep a previous file "), Choice(False, "Delete all previous files")], - vi_mode=manager.vi_mode, - ).execute() - - delete_off_disk = inquirer.select( - message="How to handle removal of files: ", - default=manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["delete_off_disk"], - choices=[Choice(True, "Permanently Delete File"), Choice(False, "Send to Trash")], - vi_mode=manager.vi_mode, - ).execute() - - manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["delete_after_download"] = delete_after - manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["hash_while_downloading"] = ( - hash_while_downloading - ) - manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["keep_prev_download"] = keep_prev - manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["keep_new_download"] = keep_current - - manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["dedupe_already_downloaded"] = ( - dedupe_already_downloaded - ) - - manager.config_manager.global_settings_data["Dupe_Cleanup_Options"]["delete_off_disk"] = delete_off_disk diff --git a/cyberdrop_dl/ui/prompts/settings_hash_prompts.py b/cyberdrop_dl/ui/prompts/settings_hash_prompts.py deleted file mode 100644 index dfbb5436d..000000000 --- a/cyberdrop_dl/ui/prompts/settings_hash_prompts.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -import os -from typing import TYPE_CHECKING - -from InquirerPy import inquirer -from InquirerPy.validator import PathValidator - -if TYPE_CHECKING: - from pathlib import Path - - -def path_prompt() -> Path: - home_path = "~/" if os.name == "posix" else "C:\\" - return inquirer.filepath( - message="Select the directory to scan", - default=home_path, - validate=PathValidator(is_dir=True, message="Input is not a directory"), - ).execute() diff --git a/cyberdrop_dl/ui/prompts/settings_user_prompts.py b/cyberdrop_dl/ui/prompts/settings_user_prompts.py deleted file mode 100644 index d2c420a03..000000000 --- a/cyberdrop_dl/ui/prompts/settings_user_prompts.py +++ /dev/null @@ -1,600 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING - -from InquirerPy import inquirer -from InquirerPy.base.control import Choice -from InquirerPy.validator import EmptyInputValidator, NumberValidator, PathValidator -from rich.console import Console - -from cyberdrop_dl.utils.dataclasses.supported_domains import SupportedDomains - -if TYPE_CHECKING: - from cyberdrop_dl.managers.manager import Manager - -console = Console() - - -def create_new_config_prompt(manager: Manager) -> None: - """Create a new config file.""" - console.clear() - console.print("Create a new config file") - config_name = inquirer.text( - message="Enter the name of the config:", - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - if (manager.path_manager.config_dir / config_name).is_dir(): - console.print(f"Config with name '{config_name}' already exists!") - inquirer.confirm(message="Press enter to return to the main menu.").execute() - return - manager.config_manager.change_config(config_name) - edit_config_values_prompt(manager) - - -def edit_config_values_prompt(manager: Manager) -> None: - """Edit the config values.""" - config = manager.config_manager.settings_data - - while True: - console.clear() - console.print("Editing Config Values") - action = inquirer.select( - message="What would you like to do?", - choices=[ - Choice(1, "Edit Download Options"), - Choice(2, "Edit Input / Output File Paths"), - Choice(3, "Edit Log File Naming / Path"), - Choice(4, "Edit File Size Limits"), - Choice(5, "Edit Ignore Options"), - Choice(6, "Edit Runtime Options"), - Choice(7, "Edit Sorting Options"), - Choice(8, "Edit Cookie Extraction Options"), - Choice(9, "Done"), - ], - long_instruction="ARROW KEYS: Navigate | ENTER: Select", - vi_mode=manager.vi_mode, - ).execute() - - # Edit Download Options - if action == 1: - edit_download_options_prompt(manager, config) - - # Edit Input / Output File Paths - elif action == 2: - edit_input_output_file_paths_prompt(manager, config) - - # Edit Log File Naming / Path - elif action == 3: - edit_log_file_naming_path_prompt(manager, config) - - # Edit File Size Limits - elif action == 4: - edit_file_size_limits_prompt(manager, config) - - # Edit Ignore Options - elif action == 5: - edit_ignore_options_prompt(manager, config) - - # Edit Runtime Options - elif action == 6: - edit_runtime_options_prompt(manager, config) - - # Edit Sorting Options - elif action == 7: - edit_sort_options_prompt(manager, config) - - # Edit Cookie extraction Options - elif action == 8: - edit_cookies_options_prompt(manager, config) - - # Done - elif action == 9: - manager.config_manager.settings_data = config - manager.config_manager.write_updated_settings_config() - return - - -def edit_download_options_prompt(manager: Manager, config: dict) -> None: - """Edit the download options.""" - console.clear() - action = inquirer.checkbox( - message="Select the download options you want to enable:", - choices=[ - Choice( - value="block_download_sub_folders", - name="Block Download Sub Folders", - enabled=config["Download_Options"]["block_download_sub_folders"], - ), - Choice( - value="disable_download_attempt_limit", - name="Disable Download Attempt Limit", - enabled=config["Download_Options"]["disable_download_attempt_limit"], - ), - Choice( - value="disable_file_timestamps", - name="Disable File Timestamps Editing", - enabled=config["Download_Options"]["disable_file_timestamps"], - ), - Choice( - value="include_album_id_in_folder_name", - name="Include Album ID In Folder Name", - enabled=config["Download_Options"]["include_album_id_in_folder_name"], - ), - Choice( - value="include_thread_id_in_folder_name", - name="Include Thread ID In Folder Name", - enabled=config["Download_Options"]["include_album_id_in_folder_name"], - ), - Choice( - value="remove_domains_from_folder_names", - name="Remove Domains From Folder Names", - enabled=config["Download_Options"]["remove_domains_from_folder_names"], - ), - Choice( - value="remove_generated_id_from_filenames", - name="Remove Generated ID From Filenames", - enabled=config["Download_Options"]["remove_generated_id_from_filenames"], - ), - Choice( - value="scrape_single_forum_post", - name="Scrape Single Forum Post", - enabled=config["Download_Options"]["scrape_single_forum_post"], - ), - Choice( - value="separate_posts", - name="Separate Posts Into Folders", - enabled=config["Download_Options"]["separate_posts"], - ), - Choice( - value="skip_download_mark_completed", - name="Skip Download and Mark it as Completed", - enabled=config["Download_Options"]["skip_download_mark_completed"], - ), - ], - long_instruction="ARROW KEYS: Navigate | TAB: Select | ENTER: Confirm", - vi_mode=manager.vi_mode, - ).execute() - - for key in config["Download_Options"]: - config["Download_Options"][key] = False - - for key in action: - config["Download_Options"][key] = True - - -def edit_input_output_file_paths_prompt(manager: Manager, config: dict) -> None: - """Edit the input / output file paths.""" - console.clear() - console.print("Editing Input / Output File Paths") - input_file = inquirer.filepath( - message="Enter the input file path:", - default=str(config["Files"]["input_file"]), - validate=PathValidator(is_file=True, message="Input is not a file"), - vi_mode=manager.vi_mode, - ).execute() - download_folder = inquirer.text( - message="Enter the download folder path:", - default=str(config["Files"]["download_folder"]), - validate=PathValidator(is_dir=True, message="Input is not a directory"), - vi_mode=manager.vi_mode, - ).execute() - - config["Files"]["input_file"] = Path(input_file) - config["Files"]["download_folder"] = Path(download_folder) - - -def edit_log_file_naming_path_prompt(manager: Manager, config: dict) -> None: - """Edit the log file naming / path.""" - console.clear() - console.print("Editing Log File Naming / Path") - log_folder = inquirer.filepath( - message="Enter the log folder path:", - default=str(config["Logs"]["log_folder"]), - validate=PathValidator(is_dir=True, message="Input is not a directory"), - vi_mode=manager.vi_mode, - ).execute() - main_log_filename = inquirer.text( - message="Enter the main log file name:", - default=config["Logs"]["main_log_filename"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - last_forum_post_filename = inquirer.text( - message="Enter the last forum post log file name:", - default=config["Logs"]["last_forum_post_filename"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - unsupported_urls_filename = inquirer.text( - message="Enter the unsupported urls log file name:", - default=config["Logs"]["unsupported_urls_filename"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - download_error_urls_filename = inquirer.text( - message="Enter the download error urls log file name:", - default=config["Logs"]["download_error_urls_filename"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - scrape_error_urls_filename = inquirer.text( - message="Enter the scrape error urls log file name:", - default=config["Logs"]["scrape_error_urls_filename"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - webhook_url = inquirer.text( - message="Enter the Discord webhook url:", - default=config["Logs"]["webhook_url"], - vi_mode=manager.vi_mode, - ).execute() - - config["Logs"]["log_folder"] = Path(log_folder) - config["Logs"]["main_log_filename"] = main_log_filename - config["Logs"]["last_forum_post_filename"] = last_forum_post_filename - config["Logs"]["unsupported_urls_filename"] = unsupported_urls_filename - config["Logs"]["download_error_urls_filename"] = download_error_urls_filename - config["Logs"]["scrape_error_urls_filename"] = scrape_error_urls_filename - config["Logs"]["webhook_url"] = webhook_url - - -def edit_file_size_limits_prompt(manager: Manager, config: dict) -> None: - """Edit the file size limits.""" - console.clear() - console.print("Editing File Size Limits") - maximum_image_size = inquirer.number( - message="Enter the maximum image size:", - default=int(config["File_Size_Limits"]["maximum_image_size"]), - validate=NumberValidator(), - long_instruction="This value is in bytes (0 is no limit)", - vi_mode=manager.vi_mode, - ).execute() - maximum_video_size = inquirer.number( - message="Enter the maximum video size:", - default=int(config["File_Size_Limits"]["maximum_video_size"]), - validate=NumberValidator(), - long_instruction="This value is in bytes (0 is no limit)", - vi_mode=manager.vi_mode, - ).execute() - maximum_other_size = inquirer.number( - message="Enter the maximum other file type size:", - default=int(config["File_Size_Limits"]["maximum_other_size"]), - validate=NumberValidator(), - long_instruction="This value is in bytes (0 is no limit)", - vi_mode=manager.vi_mode, - ).execute() - minimum_image_size = inquirer.number( - message="Enter the minimum image size:", - default=int(config["File_Size_Limits"]["minimum_image_size"]), - validate=NumberValidator(), - long_instruction="This value is in bytes (0 is no limit)", - vi_mode=manager.vi_mode, - ).execute() - minimum_video_size = inquirer.number( - message="Enter the minimum video size:", - default=int(config["File_Size_Limits"]["minimum_video_size"]), - validate=NumberValidator(), - long_instruction="This value is in bytes (0 is no limit)", - vi_mode=manager.vi_mode, - ).execute() - minimum_other_size = inquirer.number( - message="Enter the minimum other file type size:", - default=int(config["File_Size_Limits"]["minimum_other_size"]), - validate=NumberValidator(), - long_instruction="This value is in bytes (0 is no limit)", - vi_mode=manager.vi_mode, - ).execute() - - config["File_Size_Limits"]["maximum_image_size"] = int(maximum_image_size) - config["File_Size_Limits"]["maximum_video_size"] = int(maximum_video_size) - config["File_Size_Limits"]["maximum_other_size"] = int(maximum_other_size) - config["File_Size_Limits"]["minimum_image_size"] = int(minimum_image_size) - config["File_Size_Limits"]["minimum_video_size"] = int(minimum_video_size) - config["File_Size_Limits"]["minimum_other_size"] = int(minimum_other_size) - - -def edit_ignore_options_prompt(manager: Manager, config: dict) -> None: - """Edit the ignore options.""" - console.clear() - console.print("Editing Ignore Options") - action = inquirer.checkbox( - message="Select the ignore options you want to enable:", - choices=[ - Choice( - value="exclude_videos", - name="Don't download videos files", - enabled=config["Ignore_Options"]["exclude_videos"], - ), - Choice( - value="exclude_images", - name="Don't download images files", - enabled=config["Ignore_Options"]["exclude_images"], - ), - Choice( - value="exclude_audio", - name="Don't download audio files", - enabled=config["Ignore_Options"]["exclude_audio"], - ), - Choice( - value="exclude_other", - name="Don't download other files", - enabled=config["Ignore_Options"]["exclude_other"], - ), - Choice( - value="ignore_coomer_ads", - name="Ignore coomer ads when scraping", - enabled=config["Ignore_Options"]["ignore_coomer_ads"], - ), - ], - long_instruction="ARROW KEYS: Move | TAB: Select | ENTER: Confirm", - vi_mode=manager.vi_mode, - ).execute() - - for key in config["Ignore_Options"]: - config["Ignore_Options"][key] = False - - for key in action: - config["Ignore_Options"][key] = True - - skip_choices = list(SupportedDomains.supported_hosts) - skip_choices.insert(0, "None") - skip_hosts = inquirer.fuzzy( - choices=skip_choices, - multiselect=True, - message="Select any sites you want to ignore while scraping:", - long_instruction="ARROW KEYS: Move | TYPE: Filter | TAB: Select | ENTER: Confirm", - vi_mode=manager.vi_mode, - ).execute() - - skip_hosts = [host for host in skip_hosts if host in SupportedDomains.supported_hosts] - config["Ignore_Options"]["skip_hosts"] = skip_hosts - - only_choices = list(SupportedDomains.supported_hosts) - only_choices.insert(0, "None") - only_hosts = inquirer.fuzzy( - choices=only_choices, - multiselect=True, - message="Select only the sites you want to scrape from:", - long_instruction="ARROW KEYS: Move | TYPE: Filter | TAB: Select | ENTER: Confirm", - vi_mode=manager.vi_mode, - ).execute() - - only_hosts = [host for host in only_hosts if host in SupportedDomains.supported_hosts] - config["Ignore_Options"]["only_hosts"] = only_hosts - - -def edit_runtime_options_prompt(manager: Manager, config: dict) -> None: - """Edit the runtime options.""" - console.clear() - console.print("Editing Runtime Options") - action = inquirer.checkbox( - message="Select the runtime options you want to enable:", - choices=[ - Choice( - value="ignore_history", - name="Ignore the history (previously downloaded files)", - enabled=config["Runtime_Options"]["ignore_history"], - ), - Choice( - value="skip_check_for_partial_files", - name="Skip checking for partial files in the download folder", - enabled=config["Runtime_Options"]["skip_check_for_partial_files"], - ), - Choice( - value="skip_check_for_empty_folders", - name="Skip checking for empty folders in the download folder", - enabled=config["Runtime_Options"]["skip_check_for_empty_folders"], - ), - Choice( - value="delete_partial_files", - name="Delete partial files in the download folder", - enabled=config["Runtime_Options"]["delete_partial_files"], - ), - Choice( - value="send_unsupported_to_jdownloader", - name="Send unsupported urls to JDownloader to download", - enabled=config["Runtime_Options"]["send_unsupported_to_jdownloader"], - ), - Choice( - value="update_last_forum_post", - name="Update the last forum post after scraping", - enabled=config["Runtime_Options"]["update_last_forum_post"], - ), - ], - long_instruction="ARROW KEYS: Move | TAB: Select | ENTER: Confirm", - vi_mode=manager.vi_mode, - ).execute() - - log_level = inquirer.number( - message="Enter the log level:", - default=int(config["Runtime_Options"]["log_level"]), - validate=NumberValidator(), - long_instruction="10 is the default (uses pythons logging numerical levels)", - vi_mode=manager.vi_mode, - ).execute() - - console_log_level = inquirer.number( - message="Enter the log level for console output:", - default=int(config["Runtime_Options"]["console_log_level"]), - validate=NumberValidator(), - long_instruction="100 is the default, and reserved for disabling (uses pythons logging numerical levels)", - vi_mode=manager.vi_mode, - ).execute() - - for key in config["Runtime_Options"]: - config["Runtime_Options"][key] = False - - for key in action: - config["Runtime_Options"][key] = True - - config["Runtime_Options"]["log_level"] = int(log_level) - config["Runtime_Options"]["console_log_level"] = int(console_log_level) - - -def edit_sort_options_prompt(manager: Manager, config: dict) -> None: - """Edit the sort options.""" - console.clear() - console.print("Editing Sort Options") - config["Sorting"]["sort_downloads"] = False - sort_downloads = inquirer.confirm( - message="Do you want Cyberdrop-DL to sort files for you?", - vi_mode=manager.vi_mode, - ).execute() - if sort_downloads: - config["Sorting"]["sort_downloads"] = True - sort_folder = inquirer.filepath( - message="Enter the folder you want to sort files into:", - default=str(config["Sorting"]["sort_folder"]), - vi_mode=manager.vi_mode, - ).execute() - - scan_folder = inquirer.filepath( - message="Enter the folder you want to scan for files", - default=str(config["Sorting"]["scan_folder"] or config["Files"]["download_folder"]), - validate=PathValidator(is_dir=True, message="Input is not a directory"), - vi_mode=manager.vi_mode, - ).execute() - sort_incremementer_format = inquirer.text( - message="Enter the sort incrementer format:", - default=config["Sorting"]["sort_incremementer_format"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - sorted_audio = inquirer.text( - message="Enter the format you want to sort audio files into:", - default=config["Sorting"]["sorted_audio"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - sorted_video = inquirer.text( - message="Enter the format you want to sort video files into:", - default=config["Sorting"]["sorted_video"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - sorted_image = inquirer.text( - message="Enter the format you want to sort image files into:", - default=config["Sorting"]["sorted_image"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - sorted_other = inquirer.text( - message="Enter the format you want to sort other files into:", - default=config["Sorting"]["sorted_other"], - validate=EmptyInputValidator("Input should not be empty"), - vi_mode=manager.vi_mode, - ).execute() - - config["Sorting"]["sort_folder"] = Path(sort_folder) - config["Sorting"]["scan_folder"] = Path(scan_folder) if bool(scan_folder) else None - config["Sorting"]["sort_incremementer_format"] = sort_incremementer_format - config["Sorting"]["sorted_audio"] = sorted_audio - config["Sorting"]["sorted_video"] = sorted_video - config["Sorting"]["sorted_image"] = sorted_image - config["Sorting"]["sorted_other"] = sorted_other - - -def edit_cookies_options_prompt(manager: Manager, config: dict) -> None: - """Edit the file size limits.""" - console.clear() - console.print("Editing Automatic Cookie Extraction Settings") - auto_import = inquirer.select( - message="Toggles auto cookie extraction", - default=config["Browser_Cookies"]["auto_import"], - vi_mode=manager.vi_mode, - choices=[ - Choice( - value=False, - name="Disable auto cookie extraction", - ), - Choice( - value=True, - name="Enable auto cookie extraction", - ), - ], - ).execute() - browser_select = inquirer.checkbox( - message="Select the browser(s) for cookie extraction", - vi_mode=manager.vi_mode, - choices=[ - Choice(value="chrome", name="Chrome"), - Choice(value="firefox", name="Firefox"), - Choice(value="edge", name="Edge"), - Choice(value="safari", name="Safari"), - Choice(value="opera", name="Opera"), - Choice(value="brave", name="Brave"), - Choice(value="librewolf", name="LibreWolf"), - Choice(value="opera_gx", name="Opera GX"), - Choice(value="vivaldi", name="Vivaldi"), - Choice(value="chromium", name="Chromium"), - ], - long_instruction="ARROW KEYS: Navigate | TAB: Select | ENTER: Confirm", - ).execute() - sites_select = inquirer.checkbox( - message="Select the site for cookie extraction", - vi_mode=manager.vi_mode, - choices=[ - Choice(value="bunkr", name="bunkr"), - Choice(value="bunkrr", name="bunkrr"), - Choice(value="celebforum", name="celebforum"), - Choice(value="coomer", name="coomer"), - Choice(value="cyberdrop", name="cyberdrop"), - Choice(value="cyberfile", name="cyberfile"), - Choice(value="e-hentai", name="e-hentai"), - Choice(value="erome", name="erome"), - Choice(value="f95zone", name="f95zone"), - Choice(value="fapello", name="fapello"), - Choice(value="gofile", name="gofile"), - Choice(value="host.church", name="host.church"), - Choice(value="hotpic", name="hotpic"), - Choice(value="ibb.co", name="ibb.co"), - Choice(value="imageban", name="imageban"), - Choice(value="imagepond.net", name="imagepond.net"), - Choice(value="img.kiwi", name="img.kiwi"), - Choice(value="imgbox", name="imgbox"), - Choice(value="imgur", name="imgur"), - Choice(value="jpeg.pet", name="jpeg.pet"), - Choice(value="jpg.church", name="jpg.church"), - Choice(value="jpg.fish", name="jpg.fish"), - Choice(value="jpg.fishing", name="jpg.fishing"), - Choice(value="jpg.homes", name="jpg.homes"), - Choice(value="jpg.pet", name="jpg.pet"), - Choice(value="jpg1.su", name="jpg1.su"), - Choice(value="jpg2.su", name="jpg2.su"), - Choice(value="jpg3.su", name="jpg3.su"), - Choice(value="jpg4.su", name="jpg4.su"), - Choice(value="jpg5.su", name="jpg5.su"), - Choice(value="kemono", name="kemono"), - Choice(value="leakedmodels", name="leakedmodels"), - Choice(value="mediafire", name="mediafire"), - Choice(value="nudostar.com", name="nudostar.com"), - Choice(value="nudostar.tv", name="nudostar.tv"), - Choice(value="omegascans", name="omegascans"), - Choice(value="pimpandhost", name="pimpandhost"), - Choice(value="pixeldrain", name="pixeldrain"), - Choice(value="postimg", name="postimg"), - Choice(value="realbooru", name="realbooru"), - Choice(value="real-debrid", name="real-debrid"), - Choice(value="redd.it", name="redd.it"), - Choice(value="reddit", name="reddit"), - Choice(value="redgifs", name="redgifs"), - Choice(value="rule34.xxx", name="rule34.xxx"), - Choice(value="rule34.xyz", name="rule34.xyz"), - Choice(value="rule34vault", name="rule34vault"), - Choice(value="saint", name="saint"), - Choice(value="scrolller", name="scrolller"), - Choice(value="socialmediagirls", name="socialmediagirls"), - Choice(value="toonily", name="toonily"), - Choice(value="tokyomotion.net", name="tokyomotion.net"), - Choice(value="xbunker", name="xbunker"), - Choice(value="xbunkr", name="xbunkr"), - Choice(value="xxxbunker", name="xxxbunker"), - ], - long_instruction="ARROW KEYS: Navigate | TAB: Select | ENTER: Confirm", - ).execute() - - config["Browser_Cookies"]["auto_import"] = auto_import - config["Browser_Cookies"]["browsers"] = browser_select - config["Browser_Cookies"]["sites"] = sites_select diff --git a/cyberdrop_dl/ui/prompts/url_file_prompts.py b/cyberdrop_dl/ui/prompts/url_file_prompts.py deleted file mode 100644 index 452f92f14..000000000 --- a/cyberdrop_dl/ui/prompts/url_file_prompts.py +++ /dev/null @@ -1,30 +0,0 @@ -import re -from pathlib import Path - -from InquirerPy import inquirer -from rich.console import Console - -console = Console() - - -def edit_urls_prompt(file: Path, vi_mode: bool, fix_strings: bool = True) -> None: - """Edit the URLs file.""" - console.clear() - console.print(f"Editing URLs: {file}") - with file.open() as f: - existing_urls = f.read() - - result = inquirer.text( - message="URLs:", - multiline=True, - default=existing_urls, - long_instruction="Press escape and then enter to finish editing.", - vi_mode=vi_mode, - ).execute() - - if fix_strings: - result = result.replace(" ", "\n") - result = re.sub(r"(\n)+", "\n", result) - - with file.open("w") as f: - f.write(result) diff --git a/cyberdrop_dl/ui/prompts/user_prompts.py b/cyberdrop_dl/ui/prompts/user_prompts.py new file mode 100644 index 000000000..79fad7460 --- /dev/null +++ b/cyberdrop_dl/ui/prompts/user_prompts.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +from textwrap import dedent +from typing import TYPE_CHECKING + +from InquirerPy import get_style +from InquirerPy.base.control import Choice +from rich.console import Console + +from cyberdrop_dl import __version__ +from cyberdrop_dl.ui.prompts import basic_prompts +from cyberdrop_dl.ui.prompts.defaults import ALL_CHOICE, DONE_CHOICE, EXIT_CHOICE +from cyberdrop_dl.utils.constants import BROWSERS, RESERVED_CONFIG_NAMES +from cyberdrop_dl.utils.cookie_extraction import get_cookies_from_browsers +from cyberdrop_dl.utils.dataclasses.supported_domains import SupportedDomains +from cyberdrop_dl.utils.utilities import clear_term + +if TYPE_CHECKING: + from pathlib import Path + + from cyberdrop_dl.managers.manager import Manager + +console = Console() + + +def main_prompt(manager: Manager) -> int: + """Main prompt for the program.""" + prompt_header(manager) + OPTIONS = { + "group_1": [ + "Download", + "Retry failed downloads", + "Create file hashes", + "Sort files in download folder", + ], + "group_2": ["Edit URLs.txt", "Change config", "Edit configs", "Import V4 items"], + "group_3": [ + "Check for updates", + "View changelog", + ], + } + + choices = basic_prompts.create_choices(OPTIONS, append_last=EXIT_CHOICE) + simp_disclaimer_shown = manager.cache_manager.get("simp_disclaimer_shown") + if not simp_disclaimer_shown: + choices = [Choice(-1, "!! PRESS TO VIEW DISCLAIMER !!")] + + prompt_options = {"style": get_style({"pointer": "#ff0000 bold"}) if not simp_disclaimer_shown else None} + + if not simp_disclaimer_shown: + prompt_options["long_instruction"] = "ENTER: view disclaimer" + + return basic_prompts.ask_choice(choices, **prompt_options) + + +""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MANAGE CONFIG PROMPTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""" + + +def manage_configs(manager: Manager) -> int: + """Manage Configs Prompt.""" + prompt_header(manager) + OPTIONS = { + "group_1": [ + "Change default config", + "Create a new config", + "Delete a config", + "Clear cached responses", + ], + "group_2": [ + "Edit current config", + "Edit authentication config", + "Edit global config ", + ], + "group 3": ["Edit auto cookie extraction settings", "Import cookies now"], + } + + choices = basic_prompts.create_choices(OPTIONS) + return basic_prompts.ask_choice(choices) + + +def create_new_config(manager: Manager, *, title: str = "Create a new config file") -> str | None: + """Asks the user for a new config name. Returns `None` if the config name is invalid.""" + clear_term() + console.print(title) + answer: str = basic_prompts.ask_text("Enter the name of the config:") + return _check_valid_new_config_name(answer, manager) + + +def select_config(configs: list) -> str: + """Asks the user to select an existing config name.""" + return basic_prompts.ask_choice_fuzzy( + message="Select a config file:", + choices=configs, + validate_empty=True, + long_instruction="ARROW KEYS: Navigate | TYPE: Filter | TAB: select, ENTER: Finish Selection", + invalid_message="Need to select a config.", + ) + + +def _check_valid_new_config_name(answer: str, manager: Manager) -> str | None: + """Check if the provided config name if. Returns `None` if the config name is invalid.""" + msg = None + if answer.casefold() in RESERVED_CONFIG_NAMES: + msg = f"[bold red]ERROR:[/bold red] Config name '{answer}' is a reserved internal name" + + elif manager.path_manager.config_dir.joinpath(answer).is_dir(): + msg = f"[bold red]ERROR:[/bold red] Config with name '{answer}' already exists!" + if msg: + console.print(msg) + basic_prompts.enter_to_continue() + return None + + return answer + + +""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AUTHENTICATION PROMPTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""" + + +def auto_cookie_extraction(manager: Manager): + answer = basic_prompts.ask_toggle("Enable auto cookies import:") + manager.config_manager.settings_data["Browser_Cookies"]["auto_import"] = answer + if answer: + extract_cookies(manager, dry_run=True) + manager.config_manager.write_updated_settings_config() + + +def extract_cookies(manager: Manager, *, dry_run: bool = False) -> None: + """Asks the user to select browser(s) and domains(s) to import cookies from.""" + OPTIONS = [["forum", "file-host"]] + choices = basic_prompts.create_choices(OPTIONS) + domain_type = basic_prompts.ask_choice(choices, message="Select categorie:") + + if domain_type == DONE_CHOICE.value: + return + + all_domains = SupportedDomains.supported_forums_map.keys() if domain_type == 1 else SupportedDomains.supported_hosts + domain_choices = [Choice(site) for site in all_domains] + [ALL_CHOICE] + domains = basic_prompts.ask_checkbox(domain_choices, message="Select site(s) to import cookies from:") + browsers = browser_prompt() + + if ALL_CHOICE.value in domains: + domains = all_domains + + if ALL_CHOICE.value in browsers: + browsers = list(map(str.capitalize, BROWSERS)) + + if dry_run: + manager.config_manager.settings_data["Browser_Cookies"]["browsers"] = browsers + manager.config_manager.settings_data["Browser_Cookies"]["sites"] = domains + return + + get_cookies_from_browsers(manager, browsers=browsers, domains=domains) + console.print("Import finished", style="green") + basic_prompts.enter_to_continue() + + +def browser_prompt() -> str: + choices = [Choice(browser, browser.capitalize()) for browser in BROWSERS] + return basic_prompts.ask_checkbox(choices, message="Select the browser(s) for extraction:") + + +""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ V4 IMPORT PROMPTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""" + + +def import_cyberdrop_v4_items_prompt(manager: Manager) -> int: + """Import Cyberdrop_V4 Items.""" + prompt_header(manager) + OPTIONS = [["Import config", "Import download_history.sql"]] + choices = basic_prompts.create_choices(OPTIONS) + console.print("V4 Import Menu") + return basic_prompts.ask_choice(choices) + + +def import_v4_config_prompt(manager: Manager) -> tuple[str, Path] | None: + """Asks the user for the name and path of the config to import. Returns `None` if the config name is invalid.""" + new_config = create_new_config(manager, title="What should this config be called:") + if not new_config: + return None + return new_config, basic_prompts.ask_file_path("Select the config file to import:") + + +def import_v4_download_history_prompt() -> Path: + return basic_prompts.ask_file_path("Select the download_history.sql file to import:") + + +""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OTHERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""" + + +def prompt_header(manager: Manager, title: str | None = None) -> None: + clear_term() + title = title or f"[bold]Cyberdrop Downloader ([blue]V{__version__!s}[/blue])[/bold]" + console.print(title) + console.print(f"[bold]Current config:[/bold] [blue]{manager.config_manager.loaded_config}[/blue]") + + +""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DEPRECATED PROMPTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""" + + +def donations_prompt() -> None: + """Donations prompt.""" + clear_term() + msg = """ + [bold]Donations[/bold] + + I started making this program around three years ago at this point, + It has grown larger than I could've imagined and I'm very proud of it. + I have put a lot of time and effort into this program and I'm glad that people are using it. + Thanks to everyone that have supported me, + it keeps me motivated to continue working on this program + + If you'd like to support me and my work, you can donate to me via the following methods: + BuyMeACoffee: https://www.buymeacoffee.com/jbsparrow + Github Sponsor: https://github.com/sponsors/jbsparrow + + Thank you for your support! + """ + console.print(dedent(msg)) + basic_prompts.enter_to_continue() diff --git a/cyberdrop_dl/ui/ui.py b/cyberdrop_dl/ui/ui.py deleted file mode 100644 index 7e5d0d2b3..000000000 --- a/cyberdrop_dl/ui/ui.py +++ /dev/null @@ -1,243 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path -from textwrap import dedent -from typing import TYPE_CHECKING - -from InquirerPy import inquirer -from InquirerPy.validator import PathValidator -from requests import request -from rich import print as rprint -from rich.console import Console -from rich.markdown import Markdown - -from cyberdrop_dl import __version__ -from cyberdrop_dl.clients.hash_client import hash_directory_scanner -from cyberdrop_dl.ui.prompts.general_prompts import ( - import_cyberdrop_v4_items_prompt, - main_prompt, - manage_configs_prompt, - select_config_prompt, -) -from cyberdrop_dl.ui.prompts.settings_authentication_prompts import edit_authentication_values_prompt -from cyberdrop_dl.ui.prompts.settings_global_prompts import edit_global_settings_prompt -from cyberdrop_dl.ui.prompts.settings_hash_prompts import path_prompt -from cyberdrop_dl.ui.prompts.settings_user_prompts import create_new_config_prompt, edit_config_values_prompt -from cyberdrop_dl.ui.prompts.url_file_prompts import edit_urls_prompt -from cyberdrop_dl.utils.utilities import check_latest_pypi - -console = Console() - -if TYPE_CHECKING: - from cyberdrop_dl.managers.manager import Manager - - -def bold(text: str) -> str: - """Format a string in bold by overstriking.""" - return "".join(ch + "\b" + ch for ch in text) - - -def program_ui(manager: Manager) -> None: - """Program UI.""" - while True: - console.clear() - console.print(f"[bold]Cyberdrop Downloader (V{__version__})[/bold]") - console.print(f"[bold]Current Config:[/bold] {manager.config_manager.loaded_config}") - - action = main_prompt(manager) - - if action == -1: - simp_disclaimer = """ - \t\t[bold red]!! DISCLAIMER !![/bold red] - - - Due to the recent DDOS attacks on [italic]SimpCity[/italic], I have made some changes to [italic]Cyberdrop-DL[/italic]. - - First and foremost, we have removed support for scraping [italic]SimpCity[/italic] for the time being. I know that this will upset many of you, but hopefully, you will understand my reasoning. - - - Because of the DDOS attacks that [italic]SimpCity[/italic] has been receiving, they have been forced to implement some protective features such as using a DDOS-Guard browser check, only allowing [link=https://simpcity.su/threads/emails-august-2024.365869/]whitelisted email domains[/link] to access the website, and [link=https://simpcity.su/threads/rate-limit-429-error.397746/]new rate limits[/link]. - [italic]Cyberdrop-DL[/italic] allows a user to scrape a model's entire thread in seconds, downloading all the files that it finds. This is great but can be problematic for a few reasons: - \t- We end up downloading a lot of content that we will never view. - \t- Such large-scale scraping with no limits puts a large strain on [italic]SimpCity[/italic]'s servers, especially when they are getting DDOSed. - \t- Scraping has no benefit for [italic]SimpCity[/italic] - they gain nothing from us scraping their website. - - For those reasons, [italic]SimpCity[/italic] has decided that they don't want to allow automated thread scraping anymore, and have removed the [italic]Cyberdrop-DL[/italic] thread from their website. - I want to respect [italic]SimpCity[/italic]'s wishes, and as a result, have disabled scraping for [italic]SimpCity[/italic] links. - - In order to help reduce the impact that [italic]Cyberdrop-DL[/italic] has on other websites, I have decided to enable the [italic bold]update_last_forum_post[/italic bold] setting for all users' configs. - You can disable it again after reading through this disclaimer, however, I would recommend against it. [italic bold]update_last_forum_post[/italic bold] actually speeds up scrapes and reduces the load on websites' servers by not re-scraping entire threads and picking up where it left off last time. - - Furthermore, I have adjusted the default rate-limiting settings in an effort to reduce the impact that [italic]Cyberdrop-DL[/italic] will have on websites. - - I encourage you to be conscientious about how you use [italic]Cyberdrop-DL[/italic]. - Some tips on how to reduce the impact your use of [italic]Cyberdrop-DL[/italic] will have on a website: - \t- Try to avoid looping runs repeatedly. - \t- If you have a large URLs file, try to comb through it occasionally and get rid of items you don't want anymore, and try to run [italic]Cyberdrop-DL[/italic] less often. - \t- Avoid downloading content you don't want. It's good to scan through the content quickly to ensure it's not a bunch of stuff you're going to delete after downloading it. - - If you do want to continue downloading from SimpCity, you can use a tampermonkey script like this one: [link=https://simpcity.su/threads/forum-post-downloader-tampermonkey-script.96714/]SimpCity Tampermonkey Forum Downloader[/link] - """ - - console.clear() - simp_disclaimer = dedent(simp_disclaimer) - rprint(simp_disclaimer) - - input("Press Enter to continue...") - manager.cache_manager.save("simp_disclaimer_shown", value=True) - - # Download - if action == 1: - break - - # Download (All Configs) - if action == 2: - manager.args_manager.all_configs = True - break - - # Retry Failed Downloads - if action == 3: - manager.args_manager.retry_failed = True - break - - # Scanning folder to create hashes - if action == 4: - path = path_prompt() - hash_directory_scanner(manager, path) - - # Sort All Configs - elif action == 5: - manager.args_manager.sort_all_configs = True - manager.args_manager.all_configs = True - break - - # Edit URLs - elif action == 6: - input_file = ( - manager.config_manager.settings_data["Files"]["input_file"] - if not manager.args_manager.input_file - else manager.args_manager.input_file - ) - edit_urls_prompt(input_file, manager.vi_mode) - - # Select Config - elif action == 7: - configs = manager.config_manager.get_configs() - selected_config = select_config_prompt(manager, configs) - manager.config_manager.change_config(selected_config) - - elif action == 8: - console.clear() - console.print("Editing Input / Output File Paths") - input_file = inquirer.filepath( - message="Enter the input file path:", - default=str(manager.config_manager.settings_data["Files"]["input_file"]), - validate=PathValidator(is_file=True, message="Input is not a file"), - vi_mode=manager.vi_mode, - ).execute() - download_folder = inquirer.text( - message="Enter the download folder path:", - default=str(manager.config_manager.settings_data["Files"]["download_folder"]), - validate=PathValidator(is_dir=True, message="Input is not a directory"), - vi_mode=manager.vi_mode, - ).execute() - - manager.config_manager.settings_data["Files"]["input_file"] = Path(input_file) - manager.config_manager.settings_data["Files"]["download_folder"] = Path(download_folder) - manager.config_manager.write_updated_settings_config() - - # Manage Configs - elif action == 9: - while True: - console.clear() - console.print("[bold]Manage Configs[/bold]") - console.print(f"[bold]Current Config:[/bold] {manager.config_manager.loaded_config}") - - action = manage_configs_prompt(manager) - - # Change Default Config - if action == 1: - configs = manager.config_manager.get_configs() - selected_config = select_config_prompt(manager, configs) - manager.config_manager.change_default_config(selected_config) - - # Create A Config - elif action == 2: - create_new_config_prompt(manager) - - # Delete A Config - elif action == 3: - configs = manager.config_manager.get_configs() - if len(configs) != 1: - selected_config = select_config_prompt(manager, configs) - if selected_config == manager.config_manager.loaded_config: - inquirer.confirm( - message="You cannot delete the currently active config, press enter to continue.", - default=False, - vi_mode=manager.vi_mode, - ).execute() - continue - manager.config_manager.delete_config(selected_config) - else: - inquirer.confirm( - message="There is only one config, press enter to continue.", - default=False, - vi_mode=manager.vi_mode, - ).execute() - - # Edit Config - elif action == 4: - edit_config_values_prompt(manager) - - # Edit Authentication Values - elif action == 5: - edit_authentication_values_prompt(manager) - - # Edit Global Settings - elif action == 6: - edit_global_settings_prompt(manager) - - # Done - elif action == 7: - break - - elif action == 10: - check_latest_pypi(log_to_console=True, call_from_ui=True) - sys.exit(0) - - # Import Cyberdrop_V4 Items - elif action == 11: - import_cyberdrop_v4_items_prompt(manager) - - elif action == 12: - changelog_path = manager.path_manager.config_dir.parent / "CHANGELOG.md" - changelog_content = _get_changelog_content(changelog_path) - - with console.pager(links=True): - console.print(Markdown(changelog_content, justify="left")) - - # Exit - elif action == 13: - sys.exit(0) - - -def _get_changelog_content(changelog_path: Path) -> str: - url = "https://raw.githubusercontent.com/jbsparrow/CyberDropDownloader/refs/heads/master/CHANGELOG.md" - _, lastest_version = check_latest_pypi(log_to_console=False) - latest_changelog = changelog_path.with_name(f"{changelog_path.stem}_{lastest_version}{changelog_path.suffix}") - if not latest_changelog.is_file(): - changelog_pattern = f"{changelog_path.stem}*{changelog_path.suffix}" - for old_changelog in changelog_path.parent.glob(changelog_pattern): - old_changelog.unlink() - try: - with request("GET", url, timeout=10) as response: - response.raise_for_status() - with latest_changelog.open("wb") as f: - f.write(response.content) - except Exception: - return "UNABLE TO GET CHANGELOG INFORMATION" - - changelog_lines = latest_changelog.read_text(encoding="utf8").splitlines() - # remove keep_a_changelog disclaimer - return "\n".join(changelog_lines[:4] + changelog_lines[6:]) diff --git a/cyberdrop_dl/utils/args/browser_cookie_extraction.py b/cyberdrop_dl/utils/args/browser_cookie_extraction.py deleted file mode 100644 index 2df529c73..000000000 --- a/cyberdrop_dl/utils/args/browser_cookie_extraction.py +++ /dev/null @@ -1,134 +0,0 @@ -from __future__ import annotations - -import re -from functools import wraps -from http.cookiejar import MozillaCookieJar -from typing import TYPE_CHECKING - -from InquirerPy import inquirer -from rich.console import Console - -import cyberdrop_dl.dependencies.browser_cookie3 as browser_cookie3 -from cyberdrop_dl.dependencies.browser_cookie3 import BrowserCookieError -from cyberdrop_dl.utils.dataclasses.supported_domains import SupportedDomains - -if TYPE_CHECKING: - from collections.abc import Callable - from http.cookiejar import CookieJar - - from cyberdrop_dl.managers.manager import Manager - - -def cookie_wrapper(func: Callable) -> CookieJar: - """Wrapper handles cookie extraction errors.""" - - @wraps(func) - def wrapper(*args, **kwargs) -> CookieJar: - try: - return func(*args, **kwargs) - except PermissionError: - console = Console() - console.clear() - console.print( - "We've encountered a Permissions Error. Please close all browsers and try again.", - style="bold red", - ) - console.print( - "If you are still having issues, make sure all browsers processes are closed in a Task Manager.", - style="bold red", - ) - console.print("Nothing has been saved.", style="bold red") - raise - except ValueError as E: - console = Console() - console.clear() - if str(E) == "Value cannot be None": - console.print( - "No browser selected", - style="bold red", - ) - else: - console.print( - "The browser provided is not supported for extraction", - style="bold red", - ) - console.print("Nothing has been saved.", style="bold red") - raise - except BrowserCookieError as E: - console = Console() - console.clear() - console.print( - "browser extraction ran into an error, the selected browser may not be available on your system", - style="bold red", - ) - console.print( - str(E), - style="bold red", - ) - console.print( - "If you are still having issues, make sure all browsers processes are closed in a Task Manager.", - style="bold red", - ) - console.print("Nothing has been saved.", style="bold red") - raise - except Exception: - inquirer.confirm(message="Press enter to continue").execute() - - return wrapper - - -@cookie_wrapper -def get_cookies_from_browser(manager: Manager, browsers: str | None = None) -> None: - """Get the cookies for the supported sites.""" - manager.path_manager.cookies_dir.mkdir(exist_ok=True) - browsers = browsers or manager.config_manager.settings_data["Browser_Cookies"]["browsers"] - if isinstance(browsers, str): - browsers = re.split(r"[ ,]+", browsers) - all_sites = set(SupportedDomains.supported_hosts) - user_sites = manager.config_manager.settings_data["Browser_Cookies"]["sites"] or SupportedDomains.supported_hosts - if isinstance(user_sites, str): - user_sites = re.split(r"[ ,]+", user_sites) - for domain in user_sites: - domain = domain.lower() if domain else None - if domain not in all_sites: - continue - cookie_jar = MozillaCookieJar() - for browser in browsers: - browser = browser.lower() if browser else None - cookies = get_cookie(browser, domain) - for cookie in cookies: - cookie_jar.set_cookie(cookie) - cookie_file_path = manager.path_manager.cookies_dir / f"{domain}.txt" - cookie_jar.save(cookie_file_path, ignore_discard=True, ignore_expires=True) - - -def get_cookie(browser: str, domain: str) -> CookieJar: - """Get the cookies for a specific domain.""" - if browser == "chrome": - cookie = browser_cookie3.chrome(domain_name=domain) - elif browser == "firefox": - cookie = browser_cookie3.firefox(domain_name=domain) - elif browser == "edge": - cookie = browser_cookie3.edge(domain_name=domain) - elif browser == "safari": - cookie = browser_cookie3.safari(domain_name=domain) - elif browser == "opera": - cookie = browser_cookie3.opera(domain_name=domain) - elif browser == "brave": - cookie = browser_cookie3.brave(domain_name=domain) - elif browser == "chromium": - cookie = browser_cookie3.chromium(domain_name=domain) - elif browser == "librewolf": - cookie = browser_cookie3.librewolf(domain_name=domain) - elif browser == "opera_gx": - cookie = browser_cookie3.opera_gx(domain_name=domain) - elif browser == "vivaldi": - cookie = browser_cookie3.vivaldi(domain_name=domain) - elif browser is None: - msg = "Value cannot be None" - raise ValueError(msg) - else: - msg = "Invalid browser specified" - raise ValueError(msg) - - return cookie diff --git a/cyberdrop_dl/utils/args/config_definitions.py b/cyberdrop_dl/utils/args/config_definitions.py index 4810e6d4c..e029f3b53 100644 --- a/cyberdrop_dl/utils/args/config_definitions.py +++ b/cyberdrop_dl/utils/args/config_definitions.py @@ -126,9 +126,9 @@ "sorted_video": "{sort_dir}/{base_dir}/Videos/{filename}{ext}", }, "Browser_Cookies": { - "browsers": "Chrome", + "browsers": ["Chrome"], "auto_import": False, - "sites": None, + "sites": [], }, } diff --git a/cyberdrop_dl/utils/constants.py b/cyberdrop_dl/utils/constants.py index 92a4b9c0d..dac156444 100644 --- a/cyberdrop_dl/utils/constants.py +++ b/cyberdrop_dl/utils/constants.py @@ -37,6 +37,7 @@ class CustomHTTPStatus(IntEnum): BLOCKED_DOMAINS = ("facebook", "instagram", "fbcdn") + STYLE_TO_DIFF_FORMAT_MAP = { "default": "{}", "green": "+ {}", @@ -46,10 +47,12 @@ class CustomHTTPStatus(IntEnum): APP_STORAGE = Path("./AppData") DOWNLOAD_STORAGE = Path("./Downloads") +RESERVED_CONFIG_NAMES = ["all", "default"] +BROWSERS = ["chrome", "firefox", "safari", "edge", "opera", "brave", "librewolf", "opera_gx", "vivaldi", "chromium"] # Pypi -PRELEASE_TAGS = { +PRERELEASE_TAGS = { "dev": "Development", "pre": "Pre-Release", "post": "Post-Release", @@ -113,5 +116,11 @@ class CustomHTTPStatus(IntEnum): ".nfo", ".txt", }, - "7z": {".7z", ".tar", ".gz", ".bz2", ".zip"}, + "7z": { + ".7z", + ".tar", + ".gz", + ".bz2", + ".zip", + }, } diff --git a/cyberdrop_dl/utils/cookie_extraction.py b/cyberdrop_dl/utils/cookie_extraction.py new file mode 100644 index 000000000..1603d8bb5 --- /dev/null +++ b/cyberdrop_dl/utils/cookie_extraction.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import contextlib +from functools import wraps +from http.cookiejar import MozillaCookieJar +from textwrap import dedent +from typing import TYPE_CHECKING + +from rich.console import Console + +from cyberdrop_dl.dependencies import browser_cookie3 +from cyberdrop_dl.utils.dataclasses.supported_domains import SupportedDomains + +if TYPE_CHECKING: + from http.cookiejar import CookieJar + + from cyberdrop_dl.managers.manager import Manager + +console = Console() + + +def cookie_wrapper(func) -> None: + """Wrapper handles errors for cookie extraction.""" + + @wraps(func) + def wrapper(self, *args, **kwargs) -> None: + msg = "" + try: + return func(self, *args, **kwargs) + except PermissionError: + msg = """ + We've encountered a Permissions Error. Please close all browsers and try again + If you are still having issues, make sure all browsers processes are closed in Task Manager. + """ + msg = dedent(msg) + "\n\nNothing has been saved." + raise browser_cookie3.BrowserCookieError(msg) from None + except ValueError as e: + msg = f"{e}\n\nNothing has been saved." + raise browser_cookie3.BrowserCookieError(msg) from None + + except browser_cookie3.BrowserCookieError as e: + msg = """ + Browser extraction ran into an error, the selected browser(s) may not be available on your system + If you are still having issues, make sure all browsers processes are closed in Task Manager. + """ + msg = dedent(msg) + f"\nERROR: {e.s}\n\nNothing has been saved." + + raise browser_cookie3.BrowserCookieError(msg) from None + + return wrapper + + +@cookie_wrapper +def get_cookies_from_browsers( + manager: Manager, *, browsers: list[str] | None = None, domains: list[str] | None = None +) -> None: + if not browsers and browsers is not None: + msg = "No browser selected" + raise ValueError(msg) + if not domains and domains is not None: + msg = "No domains selected" + raise ValueError(msg) + + browsers = browsers or manager.config_manager.settings_data["Browser_Cookies"]["browsers"] + domains = domains or manager.config_manager.settings_data["Browser_Cookies"]["sites"] + browsers = list(map(str.lower, browsers)) + domains = list(map(str.lower, domains)) + + extractors = [getattr(browser_cookie3, b) for b in browsers if hasattr(browser_cookie3, b)] + + if not extractors: + msg = "None of the provided browsers is not supported for extraction" + raise ValueError(msg) + + for extractor in extractors: + for domain in domains: + cookie_jar = MozillaCookieJar() + cookies = extractor(domain_name=domain) + for cookie in cookies: + cookie_jar.set_cookie(cookie) + manager.path_manager.cookies_dir.mkdir(parents=True, exist_ok=True) + cookie_file_path = manager.path_manager.cookies_dir / f"{domain}.txt" + update_forum_config_cookies(manager, domain, cookies) + cookie_jar.save(cookie_file_path, ignore_discard=True, ignore_expires=True) + + +def update_forum_config_cookies(manager: Manager, forum: str, cookie: CookieJar) -> None: + auth_args: dict = manager.config_manager.authentication_data + if forum not in SupportedDomains.supported_forums_map: + return + forum = f"{SupportedDomains.supported_forums_map[forum]}" + with contextlib.suppress(KeyError): + auth_args["Forums"][f"{forum}_xf_user_cookie"] = cookie._cookies[forum]["/"]["xf_user"].value + auth_args["Forums"][f"{forum}_xf_user_cookie"] = cookie._cookies["www." + forum]["/"]["xf_user"].value diff --git a/cyberdrop_dl/utils/logger.py b/cyberdrop_dl/utils/logger.py index a25058ff0..02cb73f4d 100644 --- a/cyberdrop_dl/utils/logger.py +++ b/cyberdrop_dl/utils/logger.py @@ -45,10 +45,11 @@ def log_with_color(message: str, style: str, level: int, show_in_stats: bool = T constants.LOG_OUTPUT_TEXT.append_text(text.append("\n")) -def log_spacer(level: int, char: str = "-") -> None: +def log_spacer(level: int, char: str = "-", *, log_to_console: bool = True) -> None: spacer = char * min(int(constants.DEFAULT_CONSOLE_WIDTH / 2), 50) log(spacer, level) - console.print("") + if log_to_console: + console.print("") constants.LOG_OUTPUT_TEXT.append("\n", style="black") diff --git a/cyberdrop_dl/utils/sorting.py b/cyberdrop_dl/utils/sorting.py index c9ab2d3b1..744944c7f 100644 --- a/cyberdrop_dl/utils/sorting.py +++ b/cyberdrop_dl/utils/sorting.py @@ -12,8 +12,8 @@ from videoprops import get_audio_properties, get_video_properties from cyberdrop_dl.utils.constants import FILE_FORMATS -from cyberdrop_dl.utils.logger import console, log, log_with_color -from cyberdrop_dl.utils.utilities import purge_dir_tree +from cyberdrop_dl.utils.logger import log, log_with_color +from cyberdrop_dl.utils.utilities import clear_term, purge_dir_tree if TYPE_CHECKING: from cyberdrop_dl.managers.manager import Manager @@ -137,7 +137,7 @@ async def sort(self) -> None: await asyncio.sleep(1) purge_dir_tree(self.download_dir) - console.clear() + clear_term() async def get_download_folder(self) -> list[Path]: """Gets the download folder.""" diff --git a/cyberdrop_dl/utils/transfer/transfer_v4_config.py b/cyberdrop_dl/utils/transfer/transfer_v4_config.py index 1f5ef0190..fbaa81f04 100644 --- a/cyberdrop_dl/utils/transfer/transfer_v4_config.py +++ b/cyberdrop_dl/utils/transfer/transfer_v4_config.py @@ -20,7 +20,7 @@ def _load_yaml(file: Path) -> dict: return yaml.load(yaml_file.read(), Loader=yaml.FullLoader) -def transfer_v4_config(manager: Manager, old_config_path: Path, new_config_name: str) -> None: +def transfer_v4_config(manager: Manager, new_config_name: str, old_config_path: Path) -> None: """Transfers a V4 config into V5 possession.""" new_auth_data = manager.config_manager.authentication_data new_user_data = copy.deepcopy(settings) diff --git a/cyberdrop_dl/utils/utilities.py b/cyberdrop_dl/utils/utilities.py index b0fbe6bb1..5b4931bea 100644 --- a/cyberdrop_dl/utils/utilities.py +++ b/cyberdrop_dl/utils/utilities.py @@ -2,7 +2,9 @@ import contextlib import os +import platform import re +import subprocess from functools import wraps from pathlib import Path from typing import TYPE_CHECKING @@ -146,6 +148,10 @@ def remove_file_id(manager: Manager, filename: str, ext: str) -> tuple[str, str] """~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""" +def clear_term(): + os.system("cls" if os.name == "nt" else "clear") + + def parse_bytes(size: int) -> tuple[int, str]: """Get human repr of bytes as a tuple of (VALUE , UNIT).""" for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB"]: @@ -232,7 +238,7 @@ def check_latest_pypi(log_to_console: bool = True, call_from_ui: bool = False) - message = f"A new version of Cyberdrop-DL is available: [cyan]{latest_version}[/cyan]" message = Text.from_markup(message) else: - message = Text("You are currently on the latest version of Cyberdrop-DL") + message = Text.from_markup("You are currently on the latest version of Cyberdrop-DL :white_check_mark:") level = 20 if call_from_ui: @@ -244,7 +250,7 @@ def check_latest_pypi(log_to_console: bool = True, call_from_ui: bool = False) - def check_prelease_version(current_version: str, releases: list[str]) -> tuple[str, Text]: - is_prerelease = next((tag for tag in constants.PRELEASE_TAGS if tag in current_version), False) + is_prerelease = next((tag for tag in constants.PRERELEASE_TAGS if tag in current_version), False) match = re.match(constants.PRELEASE_VERSION_PATTERN, current_version) latest_testing_version = message = None @@ -261,7 +267,7 @@ def check_prelease_version(current_version: str, releases: list[str]) -> tuple[s ) ] latest_testing_version = max(rough_matches, key=lambda x: int(re.search(r"(\d+)$", x).group())) # type: ignore - ui_tag = constants.PRELEASE_TAGS.get(test_tag, "Testing").lower() + ui_tag = constants.PRERELEASE_TAGS.get(test_tag, "Testing").lower() if current_version != latest_testing_version: message = f"A new {ui_tag} version of Cyberdrop-DL is available: " @@ -368,3 +374,48 @@ async def send_webhook_message(manager: Manager) -> None: async with ClientSession() as session, session.post(url, data=form) as response: await response.text() + + +def open_in_text_editor(file_path: Path) -> bool: + """Opens file in OS text editor.""" + if platform.system() == "Darwin": + subprocess.Popen(["open", "-a", "TextEdit", file_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + elif platform.system() == "Windows": + subprocess.Popen(["notepad.exe", file_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + elif set_default_app_if_none(file_path): + subprocess.Popen(["xdg-open", file_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + raise ValueError + + +def set_default_app_if_none(file_path: Path) -> bool: + mimetype = subprocess.run( + ["xdg-mime", "query", "filetype", str(file_path)], + capture_output=True, + text=True, + check=False, + ).stdout.strip() + if not mimetype: + return False + + default_app = subprocess.run( + ["xdg-mime", "query", "default", mimetype], + capture_output=True, + text=True, + check=False, + ).stdout.strip() + if default_app: + return True + + text_default = subprocess.run( + ["xdg-mime", "query", "default", "text/plain"], + capture_output=True, + text=True, + check=False, + ).stdout.strip() + if text_default: + return subprocess.call(["xdg-mime", "default", text_default, mimetype]) + + return False From 8acd4bfce49eb16707ef3fc167b85e67ef47dd9a Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 18 Nov 2024 09:54:47 -0500 Subject: [PATCH 2/3] Update cyberdrop_dl/ui/prompts/user_prompts.py --- cyberdrop_dl/ui/prompts/user_prompts.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/cyberdrop_dl/ui/prompts/user_prompts.py b/cyberdrop_dl/ui/prompts/user_prompts.py index 79fad7460..5fdba0e7a 100644 --- a/cyberdrop_dl/ui/prompts/user_prompts.py +++ b/cyberdrop_dl/ui/prompts/user_prompts.py @@ -193,26 +193,3 @@ def prompt_header(manager: Manager, title: str | None = None) -> None: console.print(f"[bold]Current config:[/bold] [blue]{manager.config_manager.loaded_config}[/blue]") -""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DEPRECATED PROMPTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""" - - -def donations_prompt() -> None: - """Donations prompt.""" - clear_term() - msg = """ - [bold]Donations[/bold] - - I started making this program around three years ago at this point, - It has grown larger than I could've imagined and I'm very proud of it. - I have put a lot of time and effort into this program and I'm glad that people are using it. - Thanks to everyone that have supported me, - it keeps me motivated to continue working on this program - - If you'd like to support me and my work, you can donate to me via the following methods: - BuyMeACoffee: https://www.buymeacoffee.com/jbsparrow - Github Sponsor: https://github.com/sponsors/jbsparrow - - Thank you for your support! - """ - console.print(dedent(msg)) - basic_prompts.enter_to_continue() From 9a52b66785e2656eec8c89547a56fc08215129d5 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 18 Nov 2024 10:00:14 -0500 Subject: [PATCH 3/3] Update cyberdrop_dl/ui/prompts/user_prompts.py --- cyberdrop_dl/ui/prompts/user_prompts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cyberdrop_dl/ui/prompts/user_prompts.py b/cyberdrop_dl/ui/prompts/user_prompts.py index 5fdba0e7a..78fa90bbd 100644 --- a/cyberdrop_dl/ui/prompts/user_prompts.py +++ b/cyberdrop_dl/ui/prompts/user_prompts.py @@ -1,6 +1,5 @@ from __future__ import annotations -from textwrap import dedent from typing import TYPE_CHECKING from InquirerPy import get_style