├── .gitignore ├── requirements.txt ├── id_conversor.py ├── web_access ├── request_helper.py ├── downloader.py └── ygoprodeck_api.py ├── commands ├── cmd_exit.py ├── cmd_allcards.py ├── cmd_allfields.py ├── cmd_alltokens.py ├── cmd_all.py ├── utils.py ├── cmd_force.py ├── cmd_help.py ├── setup.py └── typing.py ├── input_handler.py ├── Makefile ├── README.md ├── tracker.py ├── deckread.py ├── LICENSE ├── command_handler.py ├── constants.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | .vscode/ 3 | deck/ 4 | pics/ 5 | build/ 6 | dist/ 7 | bin/ 8 | workpath_pyinstaller/ 9 | edopro-hd-downloader-venv/ 10 | 11 | hd_cards_downloader_tracker 12 | hd_fields_downloader_tracker 13 | HdDownloader.LICENSE.txt 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.4 2 | certifi==2024.2.2 3 | charset-normalizer==3.3.2 4 | idna==3.6 5 | packaging==23.2 6 | pefile==2023.2.7 7 | pyinstaller==6.3.0 8 | pyinstaller-hooks-contrib==2024.0 9 | pywin32-ctypes==0.2.2 10 | requests==2.31.0 11 | setuptools==69.0.3 12 | urllib3==2.2.0 13 | -------------------------------------------------------------------------------- /id_conversor.py: -------------------------------------------------------------------------------- 1 | from constants import ID_CONVERSION 2 | 3 | def convert_id(card_id: int) -> int: 4 | """ 5 | Converts cards id from YGOProDeck database to EDOPro database 6 | 7 | See https://github.com/NiiMiyo/EDOPro-Hd-Downloader/issues/8 8 | """ 9 | 10 | return ID_CONVERSION.get(card_id, card_id) 11 | -------------------------------------------------------------------------------- /web_access/request_helper.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | import requests 3 | from constants import REQUEST_HEADERS 4 | 5 | def make_request( 6 | url: str, 7 | params: Optional[dict[Any, Any]] = None 8 | ) -> requests.Response: 9 | 10 | return requests.get(url, headers=REQUEST_HEADERS, params=params) 11 | -------------------------------------------------------------------------------- /commands/cmd_exit.py: -------------------------------------------------------------------------------- 1 | from commands.typing import CommandReturn, DownloaderCommand 2 | 3 | 4 | def __cmd_exit_action(_: str) -> CommandReturn: 5 | print("Bye bye <3") 6 | exit(0) 7 | 8 | 9 | COMMAND_EXIT = DownloaderCommand( 10 | name="exit", 11 | help_text="closes the program", 12 | action=__cmd_exit_action 13 | ) 14 | -------------------------------------------------------------------------------- /commands/cmd_allcards.py: -------------------------------------------------------------------------------- 1 | from commands.typing import CommandReturn, DownloadCard, DownloaderCommand 2 | from web_access.ygoprodeck_api import get_all_cards 3 | 4 | 5 | def __cmd_all_cards_action(_: str) -> CommandReturn: 6 | return [ 7 | DownloadCard(c, False) 8 | for c in get_all_cards() 9 | ] 10 | 11 | 12 | COMMAND_ALLCARDS = DownloaderCommand( 13 | name="allcards", 14 | help_text="downloads all cards", 15 | action=__cmd_all_cards_action 16 | ) 17 | -------------------------------------------------------------------------------- /commands/cmd_allfields.py: -------------------------------------------------------------------------------- 1 | from commands.typing import DownloadCard, DownloaderCommand, CommandReturn 2 | from web_access.ygoprodeck_api import get_all_fields 3 | 4 | def __cmd_all_fields_action(_: str) -> CommandReturn: 5 | return [ 6 | DownloadCard(c, True) 7 | for c in get_all_fields() 8 | ] 9 | 10 | 11 | COMMAND_ALLFIELDS = DownloaderCommand( 12 | name="allfields", 13 | help_text="downloads all fields artworks", 14 | action=__cmd_all_fields_action 15 | ) -------------------------------------------------------------------------------- /commands/cmd_alltokens.py: -------------------------------------------------------------------------------- 1 | from commands.typing import DownloadCard, DownloaderCommand, CommandReturn 2 | from web_access.ygoprodeck_api import get_all_tokens 3 | 4 | def __cmd_all_tokens_action(_: str) -> CommandReturn: 5 | return [ 6 | DownloadCard(c, False, False) 7 | for c in get_all_tokens() 8 | ] 9 | 10 | 11 | COMMAND_ALLTOKENS = DownloaderCommand( 12 | name="alltokens", 13 | help_text="downloads all tokens", 14 | action=__cmd_all_tokens_action 15 | ) 16 | -------------------------------------------------------------------------------- /commands/cmd_all.py: -------------------------------------------------------------------------------- 1 | from commands.typing import CommandReturn, DownloaderCommand 2 | from command_handler import CommandHandler 3 | 4 | 5 | def __cmd_all(_: str) -> CommandReturn: 6 | allcards = CommandHandler.commands.get("allcards") 7 | allfields = CommandHandler.commands.get("allfields") 8 | 9 | return allcards.action(_) + allfields.action(_) # type: ignore 10 | 11 | 12 | COMMAND_ALL = DownloaderCommand( 13 | name="all", 14 | help_text="downloads all cards images and all fields artworks", 15 | action=__cmd_all 16 | ) 17 | -------------------------------------------------------------------------------- /input_handler.py: -------------------------------------------------------------------------------- 1 | from command_handler import CommandHandler 2 | from commands.typing import CommandReturn 3 | from deckread import get_deck 4 | 5 | def handle_input(user_input: str) -> CommandReturn: 6 | """Handles an user input and returns a `CommandReturn` according to the 7 | matching command or deck with same name. 8 | 9 | Returns `None` if couldn't find what do download 10 | """ 11 | 12 | command = CommandHandler.find_command(user_input) 13 | if command is None: 14 | return get_deck(user_input) 15 | else: 16 | return command.action(user_input) 17 | -------------------------------------------------------------------------------- /commands/utils.py: -------------------------------------------------------------------------------- 1 | def get_args(user_input: str) -> str: 2 | """Receives an user input and returns everything after the first 3 | space character replacing double spaces with single spaces 4 | """ 5 | 6 | args = [ 7 | a for a in user_input.split(" ") 8 | if a 9 | ] 10 | 11 | return " ".join(args[1:]) 12 | 13 | 14 | def get_first_word(user_input: str) -> str: 15 | """Receives an user input and returns everything before the first 16 | space character 17 | """ 18 | 19 | if not user_input: 20 | return "" 21 | 22 | return user_input.split(" ")[0] 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FILENAME = EDOPro-HD-Downloader 2 | LICENSE = LICENSE 3 | LICENSE_BIN = HdDownloader.LICENSE.txt 4 | DISTPATH = bin 5 | WORKPATH = workpath_pyinstaller 6 | ZIP_WIN = windows.zip 7 | 8 | build: 9 | pyinstaller main.py -y --distpath "$(DISTPATH)" -F --specpath "$(DISTPATH)" -n "$(FILENAME)" -c --clean --workpath "$(WORKPATH)" 10 | cp "$(LICENSE)" "$(DISTPATH)/$(LICENSE_BIN)" 11 | 12 | rm -rf "$(WORKPATH)" 13 | 14 | 7z-win: build 15 | cd "$(DISTPATH)" && 7z a "$(ZIP_WIN)" "$(FILENAME).exe" "$(LICENSE_BIN)" 16 | 17 | clean: 18 | rm -rf "hd_cards_downloader_tracker" 19 | rm -rf "hd_fields_downloader_tracker" 20 | -------------------------------------------------------------------------------- /commands/cmd_force.py: -------------------------------------------------------------------------------- 1 | from commands.typing import CommandReturn, DownloadCard, DownloaderCommand 2 | from commands.utils import get_args 3 | from input_handler import handle_input 4 | 5 | def __cmd_force_action(user_input: str) -> CommandReturn: 6 | args = get_args(user_input) 7 | cards = handle_input(args) 8 | if cards is None: 9 | return None 10 | 11 | return [ 12 | DownloadCard(c.card_id, c.artwork, True) 13 | for c in cards 14 | ] 15 | 16 | COMMAND_FORCE = DownloaderCommand( 17 | name="force", 18 | shown_name="force ", 19 | help_text="executes ignoring trackers (example: /force /allcards)", 20 | action=__cmd_force_action 21 | ) 22 | -------------------------------------------------------------------------------- /commands/cmd_help.py: -------------------------------------------------------------------------------- 1 | from commands.typing import CommandReturn, DownloaderCommand 2 | from command_handler import CommandHandler 3 | 4 | 5 | def __cmd_help_action(_: str) -> CommandReturn: 6 | cmd_column_len = 0 7 | lines: list[tuple[str, str]] = list() 8 | 9 | command_list = sorted( 10 | CommandHandler.commands.values(), 11 | key=lambda cmd: cmd.name 12 | ) 13 | 14 | for cmd in command_list: 15 | sn = cmd.get_shown_name() 16 | 17 | lines.append((sn, cmd.help_text)) 18 | cmd_column_len = max(cmd_column_len, len(sn)) 19 | 20 | print("\n".join( 21 | f"/{sn.ljust(cmd_column_len)} - {ht}" 22 | for sn, ht in lines 23 | )) 24 | 25 | return [] 26 | 27 | 28 | COMMAND_HELP = DownloaderCommand( 29 | name="help", 30 | help_text="see this text", 31 | action=__cmd_help_action 32 | ) 33 | -------------------------------------------------------------------------------- /web_access/downloader.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from os.path import join 3 | 4 | from commands.typing import DownloadCard 5 | from constants import IMAGES_BASE_URL 6 | from id_conversor import convert_id 7 | 8 | def download_image(card: DownloadCard) -> bool: 9 | """ 10 | Downloads the card image or artwork and puts in the specified folder. 11 | 12 | Returns `True` if downloads successfully, otherwise returns `False`. 13 | """ 14 | 15 | url = IMAGES_BASE_URL 16 | store_at = "./pics/" 17 | 18 | if card.artwork: 19 | url += "_cropped" 20 | store_at += "field/" 21 | url += f"/{card.card_id}.jpg" 22 | 23 | file_path = join(store_at, f"{convert_id(card.card_id)}.jpg") 24 | try: 25 | res = requests.get(url) 26 | with open(file_path, 'wb+') as f: 27 | f.write(res.content) 28 | return True 29 | except Exception as e: 30 | print(f"Error downloading '{card.card_id}': {type(e).__name__}\n{e}") 31 | return False 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EDOPro HD Downloader 2 | 3 | # Installation 4 | 5 | ## Windows 6 | 7 | Download the [latest Windows release](https://github.com/NiiMiyo/EDOPro-Hd-Downloader/releases/latest) and unzip it at your EDOPro folder (Default should be something like `C:/ProjectIgnis/`). 8 | 9 | ## Non-Windows 10 | 11 | Since I can't test non-windows compilations you will need to download the source code and compile it yourself. Then put the compiled file at your EDOPro folder. 12 | 13 | 14 | # Usage 15 | 16 | If you run the program and read the instructions you should be fine. 17 | 18 | But for short: 19 | 20 | - Insert the name of your deck (without the `.ydk` extension) when asked to download all the images of the cards in it. 21 | 22 | - Insert `/allcards` to download images for all cards. Will probably take a while. 23 | 24 | - Insert `/allfields` to download artwork of all Field Spell Cards. 25 | 26 | - Insert `/help` if you want to know anything else. 27 | 28 | # License 29 | 30 | [MIT](https://douglas-sebastian.mit-license.org) 31 | -------------------------------------------------------------------------------- /tracker.py: -------------------------------------------------------------------------------- 1 | from commands.typing import DownloadCard 2 | from constants import CARD_CACHE_PATH, FIELD_CACHE_PATH 3 | 4 | 5 | def __get_cached(is_artwork: bool): 6 | """Reads tracker file and returns a list of ids 7 | that already were downloaded""" 8 | 9 | cache = FIELD_CACHE_PATH if is_artwork else CARD_CACHE_PATH 10 | with open(cache, mode="r+", encoding="utf8") as cache_file: 11 | cards = [c.strip() for c in cache_file.readlines()] 12 | return cards 13 | 14 | def already_downloaded(card: DownloadCard): 15 | """Returns True if card with id 'card.card_id' was 16 | already downloaded""" 17 | 18 | cards_downloaded = __get_cached(card.artwork) 19 | return str(card.card_id) in cards_downloaded 20 | 21 | def mark_as_downloaded(card: DownloadCard): 22 | """Opens tracker file to add an id to the downloaded list""" 23 | 24 | cache = FIELD_CACHE_PATH if card.artwork else CARD_CACHE_PATH 25 | 26 | with open(cache, mode="a+", encoding="utf8") as cache_file: 27 | cache_file.write(f"{card.card_id}\n") 28 | -------------------------------------------------------------------------------- /deckread.py: -------------------------------------------------------------------------------- 1 | from os.path import exists as __exists, join as __join 2 | 3 | from commands.typing import CommandReturn, DownloadCard 4 | 5 | deck_folder_path = "./deck/" 6 | 7 | 8 | def __filter_card_id(cards: list[str]) -> list[int]: 9 | """Filters an list with card ids to remove 10 | repeating ones and non-ids""" 11 | 12 | ids: list[int] = list() 13 | for c in cards: 14 | try: 15 | c = int(c) 16 | if c not in ids: 17 | ids.append(int(c)) 18 | except ValueError: 19 | continue 20 | return ids 21 | 22 | 23 | def get_deck(deck_name: str) -> CommandReturn: 24 | """Reads a deck file and returns the 25 | ids of the cards in it""" 26 | 27 | deck_path = __join(deck_folder_path, f"{deck_name}.ydk") 28 | deck_exists = __exists(deck_path) 29 | if not deck_exists: 30 | return None 31 | deck = open(deck_path, mode="r", encoding="utf8") 32 | cards = __filter_card_id([l.strip() for l in deck.readlines()]) 33 | return [ 34 | DownloadCard(c, False) 35 | for c in cards 36 | ] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2024 Douglas Sebastian 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /commands/setup.py: -------------------------------------------------------------------------------- 1 | from command_handler import CommandHandler 2 | from commands.cmd_allcards import COMMAND_ALLCARDS 3 | from commands.cmd_all import COMMAND_ALL 4 | from commands.cmd_allfields import COMMAND_ALLFIELDS 5 | from commands.cmd_alltokens import COMMAND_ALLTOKENS 6 | from commands.cmd_exit import COMMAND_EXIT 7 | from commands.cmd_force import COMMAND_FORCE 8 | from commands.cmd_help import COMMAND_HELP 9 | from constants import SETUP_CREATION_FILES, SETUP_CREATION_FOLDERS 10 | 11 | from os.path import exists 12 | from os import makedirs 13 | 14 | def setup_commands(): 15 | CommandHandler.add_command(COMMAND_ALL) 16 | CommandHandler.add_command(COMMAND_ALLCARDS) 17 | CommandHandler.add_command(COMMAND_ALLFIELDS) 18 | CommandHandler.add_command(COMMAND_EXIT) 19 | CommandHandler.add_command(COMMAND_FORCE) 20 | CommandHandler.add_command(COMMAND_HELP) 21 | CommandHandler.add_command(COMMAND_ALLTOKENS) 22 | 23 | def setup_files(): 24 | for f in SETUP_CREATION_FILES: 25 | if not exists(f): 26 | with open(f, "w+"): 27 | # I only need that the files exist 28 | pass 29 | 30 | def setup_folders(): 31 | for f in SETUP_CREATION_FOLDERS: 32 | if not exists(f): 33 | makedirs(f) 34 | -------------------------------------------------------------------------------- /command_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from commands.typing import DownloaderCommand 3 | from commands.utils import get_first_word 4 | 5 | 6 | class CommandHandler: 7 | """Class to manage the use of commands""" 8 | 9 | commands: dict[str, DownloaderCommand] = dict() 10 | """Dict of all available commands (maps command name to command). 11 | 12 | If you add a command you should put it here using 13 | `CommandHandler.add_command` and adding the import on `commands/setup.py`. 14 | """ 15 | 16 | @staticmethod 17 | def find_command(user_input: str) -> Optional[DownloaderCommand]: 18 | """Gets the `DownloaderCommand` that matches user_input. For example, 19 | `force` command for `"/force /allcards"` input. 20 | """ 21 | 22 | if not user_input: # If empty string or None 23 | return None 24 | 25 | command_used = get_first_word(user_input) 26 | if command_used.startswith("/"): 27 | return CommandHandler.commands.get(command_used[1:]) 28 | else: 29 | return None 30 | 31 | @staticmethod 32 | def add_command(command: DownloaderCommand): 33 | """Adds a DownloaderCommand to be available to use on user input""" 34 | 35 | CommandHandler.commands[command.name] = command 36 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | DOWNLOADER_VERSION = "2.3" 2 | """Program version""" 3 | 4 | REQUEST_HEADERS = { 5 | "User-Agent": f"NiiMiyo-EDOPro-HD-Downloader/{DOWNLOADER_VERSION}" 6 | } 7 | """Header to be used in an HTTP request""" 8 | 9 | INTRO_STRING = f"""EDOPro HD Downloader v{DOWNLOADER_VERSION} 10 | Created by Nii Miyo 11 | Type "/help" for help""" 12 | """String to be used when starting the program""" 13 | 14 | INPUT_STRING = "Insert deck name (without .ydk) or command: " 15 | """String that appears at user input""" 16 | 17 | YGOPRODECK_CARDS_URL = "https://db.ygoprodeck.com/api/v7/cardinfo.php" 18 | """Base API URL for YGOProDeck""" 19 | 20 | IMAGES_BASE_URL = "https://images.ygoprodeck.com/images/cards" 21 | """Base URL for images""" 22 | 23 | CARD_CACHE_PATH = "./hd_cards_downloader_tracker" 24 | """Path to the cards cache file""" 25 | 26 | FIELD_CACHE_PATH = "./hd_fields_downloader_tracker" 27 | """Path to the fields cache file""" 28 | 29 | SETUP_CREATION_FILES = (CARD_CACHE_PATH, FIELD_CACHE_PATH) 30 | """Files needed on setup""" 31 | 32 | SETUP_CREATION_FOLDERS = ("pics/field",) 33 | """Folders needed on setup""" 34 | 35 | SLEEP_TIME_BETWEEN_DOWNLOADS = 1 / 18 36 | """Time in seconds that the program will wait before downloading the next card""" 37 | 38 | ID_CONVERSION: dict[int, int] = { 39 | # Mecha Phantom Beast Token 40 | 904186: 31533705 41 | } 42 | -------------------------------------------------------------------------------- /commands/typing.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, NamedTuple, Optional 2 | 3 | 4 | class DownloadCard(NamedTuple): 5 | """Represents a card to be downloaded""" 6 | 7 | card_id: int 8 | artwork: bool 9 | force: bool = False 10 | 11 | 12 | CommandReturn = Optional[list[DownloadCard]] 13 | """Type a `CommandAction` should return""" 14 | 15 | CommandAction = Callable[[str], CommandReturn] 16 | """Type a `DownloaderCommand.action` should be. 17 | 18 | Should only return `None` in case the command fails. If the command does not 19 | download cards, return an empty list. 20 | """ 21 | 22 | 23 | class DownloaderCommand(NamedTuple): 24 | name: str 25 | """The name of the command, should be unique""" 26 | 27 | help_text: str 28 | """Text the command shows on `/help`""" 29 | 30 | action: CommandAction 31 | """Function that defines command execution""" 32 | 33 | shown_name: Optional[str] = None 34 | """Name that will be shown on `/help`. If it's `None` then shows `name`""" 35 | 36 | def match_string(self) -> str: 37 | """Returns `/command.name`""" 38 | return f"/{self.name}" 39 | 40 | def get_shown_name(self) -> str: 41 | """Returns `command.shown_name` if it's not None. Otherwise returns 42 | `command.name` 43 | """ 44 | 45 | if self.shown_name is None: 46 | return self.name 47 | else: 48 | return self.shown_name 49 | -------------------------------------------------------------------------------- /web_access/ygoprodeck_api.py: -------------------------------------------------------------------------------- 1 | from requests import Response 2 | from constants import YGOPRODECK_CARDS_URL 3 | from web_access.request_helper import make_request 4 | 5 | 6 | def _get_ids_from_response(response: Response) -> list[int]: 7 | """Returns only the ids of the cards requested""" 8 | data = response.json().get("data") 9 | ids: list[int] = list() 10 | 11 | for c in data: 12 | for img in c.get("card_images"): 13 | ids.append(img.get("id")) 14 | return ids 15 | 16 | def get_all_cards() -> list[int]: 17 | """Returns the ids of all Yu-Gi-Oh! cards in `db.ygoprodeck.com` database""" 18 | 19 | try: 20 | response = make_request(YGOPRODECK_CARDS_URL) 21 | return _get_ids_from_response(response) 22 | except Exception as e: 23 | print(f"Error fetching db.ygoprodeck.com: {type(e).__name__}\n{e}") 24 | 25 | return list() 26 | 27 | def get_all_fields() -> list[int]: 28 | """ 29 | Returns the ids of all Yu-Gi-Oh! Field Spell cards in 30 | `db.ygoprodeck.com` database 31 | """ 32 | 33 | try: 34 | response = make_request( 35 | YGOPRODECK_CARDS_URL, 36 | params={"type": "spell card","race": "field"} 37 | ) 38 | return _get_ids_from_response(response) 39 | except Exception as e: 40 | print(f"Error fetching db.ygoprodeck.com: {type(e).__name__}\n{e}") 41 | 42 | return list() 43 | 44 | def get_all_tokens() -> list[int]: 45 | """ 46 | Return the ids of all Tokens in the `db.ygoprodeck.com` database 47 | """ 48 | 49 | try: 50 | response = make_request( 51 | YGOPRODECK_CARDS_URL, 52 | params={"type": "token"} 53 | ) 54 | return _get_ids_from_response(response) 55 | except Exception as e: 56 | print(f"Error fetching db.ygoprodeck.com: {type(e).__name__}\n{e}") 57 | 58 | return list() 59 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from traceback import print_exc 3 | from commands.setup import setup_commands, setup_files, setup_folders 4 | from constants import INPUT_STRING, INTRO_STRING, SLEEP_TIME_BETWEEN_DOWNLOADS 5 | 6 | from input_handler import handle_input 7 | from commands.typing import DownloadCard 8 | from web_access.downloader import download_image 9 | from tracker import (already_downloaded, mark_as_downloaded) 10 | 11 | 12 | def initialize(): 13 | """Creates tracker files if they do not exist, setups all commands and 14 | introduces the program 15 | """ 16 | 17 | setup_files() 18 | setup_folders() 19 | setup_commands() 20 | print(INTRO_STRING) 21 | 22 | 23 | def to_download(card: DownloadCard): 24 | """Handles if a card should be downloaded and downloads it.""" 25 | 26 | if (card.force) or (not already_downloaded(card)): 27 | success = download_image(card) 28 | if success: mark_as_downloaded(card) 29 | sleep(SLEEP_TIME_BETWEEN_DOWNLOADS) 30 | 31 | 32 | def main(): 33 | initialize() 34 | 35 | try: 36 | while True: 37 | cards = handle_input( input(INPUT_STRING) ) 38 | 39 | # If couldn't find what to download 40 | if cards is None: 41 | print("Deck or command not found.") 42 | continue 43 | 44 | total_cards = len(cards) 45 | 46 | # For each card, download 47 | for index, card in enumerate(cards, 1): 48 | to_download(card) 49 | 50 | # Prints progress 51 | raw_progress = f"{index}/{total_cards}" 52 | percentage = f"{((index * 100) / total_cards):.2f}%" 53 | print(f"Downloaded {raw_progress} - {percentage}", end="\r") 54 | 55 | print("\n") 56 | 57 | # In case of interrupting the program with Ctrl+C 58 | except KeyboardInterrupt: print("\n\nForcing program interruption...") 59 | 60 | # In case of a not expected exception 61 | except Exception: print_exc() 62 | 63 | 64 | if __name__ == "__main__": main() 65 | --------------------------------------------------------------------------------