diff --git a/.gitignore b/.gitignore index 36ae41f..5dab794 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ pics/ build/ dist/ bin/ +workpath_pyinstaller/ hd_cards_downloader_tracker hd_fields_downloader_tracker diff --git a/Makefile b/Makefile index fd9c104..01c972b 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ FILENAME = EDOPro-HD-Downloader LICENSE = HdDownloader.LICENSE.txt DISTPATH = ./bin +WORKPATH = ./workpath_pyinstaller build: - pyinstaller main.py -y --distpath "$(DISTPATH)" -F --specpath "$(DISTPATH)" -n "$(FILENAME)" -c --clean - cp $(LICENSE) $(DISTPATH)/$(LICENSE) + pyinstaller main.py -y --distpath "$(DISTPATH)" -F --specpath "$(DISTPATH)" -n "$(FILENAME)" -c --clean --workpath "$(WORKPATH)" + cp "$(LICENSE)" "$(DISTPATH)/$(LICENSE)" - rm -rf build + rm -rf "$(WORKPATH)" diff --git a/README.md b/README.md index 9feb501..1c22f8b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you run the program and read the instructions you should be fine. But for short: -- Insert the name of your deck (without the `.ydk`) extension when asked to download all the images of the cards in it. +- Insert the name of your deck (without the `.ydk` extension) when asked to download all the images of the cards in it. - Insert `/allcards` to download images for all cards. Will probably take a while. diff --git a/apiaccess.py b/apiaccess.py index 2bac9d0..cf89636 100644 --- a/apiaccess.py +++ b/apiaccess.py @@ -2,11 +2,13 @@ from urllib import request as __request from json import loads as __loads -api_url = "https://db.ygoprodeck.com/api/v7/cardinfo.php" -__headers = { +API_URL = "https://db.ygoprodeck.com/api/v7/cardinfo.php" +"""Base API URL for YGOProDeck""" + +API_HEADER = { "User-Agent": "NiiMiyo-EDOPro-HD-Downloader/1.1.1" } - +"""Header JSON to be used in an API request""" def __get_ids_from_api_response(response: __HTTPResponse) -> list[int]: """Returns only the ids of the cards requested""" @@ -23,10 +25,10 @@ def __get_ids_from_api_response(response: __HTTPResponse) -> list[int]: def get_all_cards() -> list[int]: """Returns the ids of all Yu-Gi-Oh! cards in - db.ygoprodeck.com database""" + `db.ygoprodeck.com` database""" try: - request = __request.Request(api_url, headers=__headers) + request = __request.Request(API_URL, headers=API_HEADER) response = __request.urlopen(request) except Exception as e: print(f"Error fetching db.ygoprodeck.com: {e}") @@ -37,11 +39,11 @@ def get_all_cards() -> list[int]: def get_all_fields() -> list[int]: """Returns the ids of all Yu-Gi-Oh! Field Spell cards in - db.ygoprodeck.comdatabase""" + `db.ygoprodeck.com` database""" try: params = r"?type=spell%20card&race=field" - request = __request.Request(api_url + params, headers=__headers) + request = __request.Request(API_URL + params, headers=API_HEADER) response = __request.urlopen(request) except Exception as e: print(e) diff --git a/command_handler.py b/command_handler.py new file mode 100644 index 0000000..501fd69 --- /dev/null +++ b/command_handler.py @@ -0,0 +1,35 @@ +from typing import Optional +from commands.typing import DownloaderCommand +from commands.utils import get_first_word + + +class CommandHandler: + """Class to manage the use of commands""" + + commands: dict[str, DownloaderCommand] = dict() + """Dict of all available commands (maps command name to command). + + If you add a command you should put it here using + `CommandHandler.add_command` and adding the import on `commands/setup.py`. + """ + + @staticmethod + def find_command(user_input: str) -> Optional[DownloaderCommand]: + """Gets the `DownloaderCommand` that matches user_input. For example, + `force` command for `"/force /allcards"` input. + """ + + if not user_input: # If empty string or None + return None + + command_used = get_first_word(user_input) + if command_used.startswith("/"): + return CommandHandler.commands.get(command_used[1:]) + else: + return None + + @staticmethod + def add_command(command: DownloaderCommand): + """Adds a DownloaderCommand to be available to use on user input""" + + CommandHandler.commands[command.name] = command diff --git a/commands/cmd_all.py b/commands/cmd_all.py new file mode 100644 index 0000000..8a65144 --- /dev/null +++ b/commands/cmd_all.py @@ -0,0 +1,16 @@ +from commands.typing import CommandReturn, DownloaderCommand +from command_handler import CommandHandler + + +def __cmd_all(_: str) -> CommandReturn: + allcards = CommandHandler.commands.get("allcards") + allfields = CommandHandler.commands.get("allfields") + + return allcards.action(_) + allfields.action(_) # type: ignore + + +CommandHandler.add_command(DownloaderCommand( + name="all", + help_text="downloads all cards images and all fields artworks", + action=__cmd_all +)) diff --git a/commands/cmd_allcards.py b/commands/cmd_allcards.py new file mode 100644 index 0000000..1e64ddf --- /dev/null +++ b/commands/cmd_allcards.py @@ -0,0 +1,17 @@ +from command_handler import CommandHandler +from commands.typing import CommandReturn, DownloadCard, DownloaderCommand +from apiaccess import get_all_cards + + +def __cmd_all_cards_action(_: str) -> CommandReturn: + return [ + DownloadCard(c, False) + for c in get_all_cards() + ] + + +CommandHandler.add_command(DownloaderCommand( + name="allcards", + help_text="downloads all cards", + action=__cmd_all_cards_action +)) diff --git a/commands/cmd_allfields.py b/commands/cmd_allfields.py new file mode 100644 index 0000000..2653faf --- /dev/null +++ b/commands/cmd_allfields.py @@ -0,0 +1,17 @@ +from command_handler import CommandHandler +from commands.typing import DownloadCard, DownloaderCommand, CommandReturn +from apiaccess import get_all_fields + + +def __cmd_all_fields_action(_: str) -> CommandReturn: + return [ + DownloadCard(c, True) + for c in get_all_fields() + ] + + +CommandHandler.add_command(DownloaderCommand( + name="allfields", + help_text="downloads all fields artworks", + action=__cmd_all_fields_action +)) diff --git a/commands/cmd_exit.py b/commands/cmd_exit.py new file mode 100644 index 0000000..c5ca167 --- /dev/null +++ b/commands/cmd_exit.py @@ -0,0 +1,14 @@ +from command_handler import CommandHandler +from commands.typing import CommandReturn, DownloaderCommand + + +def __cmd_exit_action(_: str) -> CommandReturn: + print("Bye bye <3") + exit(0) + + +CommandHandler.add_command(DownloaderCommand( + name="exit", + help_text="closes the program", + action=__cmd_exit_action +)) diff --git a/commands/cmd_force.py b/commands/cmd_force.py new file mode 100644 index 0000000..d37a430 --- /dev/null +++ b/commands/cmd_force.py @@ -0,0 +1,22 @@ +from command_handler import CommandHandler +from commands.typing import CommandReturn, DownloadCard, DownloaderCommand +from commands.utils import get_args +from input_handler import handle_input + +def __cmd_force_action(user_input: str) -> CommandReturn: + args = get_args(user_input) + cards = handle_input(args) + if cards is None: + return None + + return [ + DownloadCard(c.card_id, c.artwork, True) + for c in cards + ] + +CommandHandler.add_command(DownloaderCommand( + name="force", + shown_name="force ", + help_text="executes ignoring trackers (example: /force /allcards)", + action=__cmd_force_action +)) diff --git a/commands/cmd_help.py b/commands/cmd_help.py new file mode 100644 index 0000000..ee71291 --- /dev/null +++ b/commands/cmd_help.py @@ -0,0 +1,32 @@ +from commands.typing import CommandReturn, DownloaderCommand +from command_handler import CommandHandler + + +def __cmd_help_action(_: str) -> CommandReturn: + cmd_column_len = 0 + lines: list[tuple[str, str]] = list() + + command_list = sorted( + CommandHandler.commands.values(), + key=lambda cmd: cmd.name + ) + + for cmd in command_list: + sn = cmd.get_shown_name() + + lines.append((sn, cmd.help_text)) + cmd_column_len = max(cmd_column_len, len(sn)) + + print("\n".join( + f"/{sn.ljust(cmd_column_len)} - {ht}" + for sn, ht in lines + )) + + return [] + + +CommandHandler.add_command(DownloaderCommand( + name="help", + help_text="see this text", + action=__cmd_help_action +)) diff --git a/commands/setup.py b/commands/setup.py new file mode 100644 index 0000000..35a2b73 --- /dev/null +++ b/commands/setup.py @@ -0,0 +1,9 @@ +# Yes, this is correct +# I'm sorry +def setup_commands(): + import commands.cmd_allcards as _ + import commands.cmd_allfields as _ + import commands.cmd_exit as _ + import commands.cmd_force as _ + import commands.cmd_help as _ + import commands.cmd_all as _ diff --git a/commands/typing.py b/commands/typing.py new file mode 100644 index 0000000..e77bc8f --- /dev/null +++ b/commands/typing.py @@ -0,0 +1,48 @@ +from typing import Callable, NamedTuple, Optional + + +class DownloadCard(NamedTuple): + """Represents a card to be downloaded""" + + card_id: int + artwork: bool + force: bool = False + + +CommandReturn = Optional[list[DownloadCard]] +"""Type a `CommandAction` should return""" + +CommandAction = Callable[[str], CommandReturn] +"""Type a `DownloaderCommand.action` should be. + +Should only return `None` in case the command fails. If the command does not +download cards, return an empty list. +""" + + +class DownloaderCommand(NamedTuple): + name: str + """The name of the command, should be unique""" + + help_text: str + """Text the command shows on `/help`""" + + action: CommandAction + """Function that defines command execution""" + + shown_name: Optional[str] = None + """Name that will be shown on `/help`. If it's `None` then shows `name`""" + + def match_string(self) -> str: + """Returns `/command.name`""" + return f"/{self.name}" + + def get_shown_name(self) -> str: + """Returns `command.shown_name` if it's not None. Otherwise returns + `command.name` + """ + + if self.shown_name is None: + return self.name + else: + return self.shown_name diff --git a/commands/utils.py b/commands/utils.py new file mode 100644 index 0000000..f76a609 --- /dev/null +++ b/commands/utils.py @@ -0,0 +1,22 @@ +def get_args(user_input: str) -> str: + """Receives an user input and returns everything after the first + space character replacing double spaces with single spaces + """ + + args = [ + a for a in user_input.split(" ") + if a + ] + + return " ".join(args[1:]) + + +def get_first_word(user_input: str) -> str: + """Receives an user input and returns everything before the first + space character + """ + + if not user_input: + return "" + + return user_input.split(" ")[0] diff --git a/deckread.py b/deckread.py index 021f076..6a47e8c 100644 --- a/deckread.py +++ b/deckread.py @@ -1,5 +1,6 @@ from os.path import exists as __exists, join as __join -from typing import Optional + +from commands.typing import CommandReturn, DownloadCard deck_folder_path = "./deck/" @@ -11,16 +12,15 @@ def __filter_card_id(cards: list[str]) -> list[int]: ids: list[int] = list() for c in cards: try: - int(c) - except ValueError: - continue - else: + c = int(c) if c not in ids: ids.append(int(c)) + except ValueError: + continue return ids -def get_deck(deck_name: str) -> Optional[list[int]]: +def get_deck(deck_name: str) -> CommandReturn: """Reads a deck file and returns the ids of the cards in it""" @@ -30,4 +30,7 @@ def get_deck(deck_name: str) -> Optional[list[int]]: return None deck = open(deck_path, mode="r", encoding="utf8") cards = __filter_card_id([l.strip() for l in deck.readlines()]) - return cards + return [ + DownloadCard(c, False) + for c in cards + ] diff --git a/downloader.py b/downloader.py index 42699c1..980f29e 100644 --- a/downloader.py +++ b/downloader.py @@ -1,23 +1,25 @@ from urllib import request as __request from os.path import join as __join -def download_image(card_id: int, is_artwork: bool = False): +from commands.typing import DownloadCard + +def download_image(card: DownloadCard): """Downloads the card image or artwork and puts in the specified folder""" img_url = "https://storage.googleapis.com/ygoprodeck.com/pics" - if not is_artwork: - img_url += f"/{card_id}.jpg" + if not card.artwork: + img_url += f"/{card.card_id}.jpg" store_at = "./pics/" else: - img_url += f"_artgame/{card_id}.jpg" + img_url += f"_artgame/{card.card_id}.jpg" store_at = "./pics/field/" - file_path = __join(store_at, f"{card_id}.jpg") + file_path = __join(store_at, f"{card.card_id}.jpg") try: __request.urlretrieve(img_url, file_path) return True except Exception as e: - print(f"Error downloading '{card_id}': {e}") + print(f"Error downloading '{card.card_id}': {e}") return False diff --git a/input_handler.py b/input_handler.py new file mode 100644 index 0000000..f256291 --- /dev/null +++ b/input_handler.py @@ -0,0 +1,16 @@ +from command_handler import CommandHandler +from commands.typing import CommandReturn +from deckread import get_deck + +def handle_input(user_input: str) -> CommandReturn: + """Handles an user input and returns a `CommandReturn` according to the + matching command or deck with same name. + + Returns `None` if couldn't find what do download + """ + + command = CommandHandler.find_command(user_input) + if command is None: + return get_deck(user_input) + else: + return command.action(user_input) diff --git a/main.py b/main.py index 56c9853..82ac376 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,10 @@ from os.path import exists from time import sleep from traceback import print_exc -from typing import Optional +from commands.setup import setup_commands -from apiaccess import get_all_cards, get_all_fields -from deckread import get_deck +from input_handler import handle_input +from commands.typing import DownloadCard from downloader import download_image from tracker import (already_downloaded, card_cache_path, field_cache_path, mark_as_downloaded) @@ -12,14 +12,18 @@ # String that appears at user input INPUT_STRING = "Insert deck name (without .ydk) or command: " - -# Creates tracker files if they do not exist and introduces the program def initialize(): + """Creates tracker files if they do not exist, setups all commands and + introduces the program + """ + global card_cache_path, field_cache_path for i in card_cache_path, field_cache_path: if not exists(i): open(i, "w+").close() + setup_commands() + print("\n".join([ "EDOPro HD Downloader", "Created by Nii Miyo", @@ -27,58 +31,12 @@ def initialize(): ])) -# Handles what to do with user input -def handle_input(_input: str) -> tuple[Optional[list[int]], bool, bool]: - """Should return a tuple which the first element is a list with cards to - download and the second is a boolean indicating if should download only - the artwork at fields folder""" - - _input = _input.strip() - - # Downloads all cards images - if _input == "/allcards": - return get_all_cards(), False, False - - # Downloads all field spell cards artworks - elif _input == "/allfields": - return get_all_fields(), True, False - - # Help command - elif _input == "/help": - print("\n".join([ - "Press Ctrl+C while downloading to force-stop the program", - "Available commands:", - "/allcards - downloads all cards", - "/allfields - downloads all fields artworks", - "/force - executes ignoring trackers", - "/exit - closes the program", - "/help - see this text", - ]), end="") - - # Force command - elif _input.startswith("/force "): - response = handle_input(_input[7:]) - return response[0], response[1], True - - # Closes the program - elif _input == "/exit": - print("Bye bye <3") - exit(0) - - # Since none of the commands where triggered, searchs for a deck - # which name equals input - else: - return get_deck(_input), False, False - - # Default return for non-download commands - return list(), False, False - - -# Handles if a card should be downloaded -def to_download(card_id: int, is_artwork: bool = False, force: bool = False): - if (force) or (not already_downloaded(card_id, is_artwork)): - download_image(card_id, is_artwork) - mark_as_downloaded(card_id, is_artwork) +def to_download(card: DownloadCard): + """Handles if a card should be downloaded and downloads it.""" + + if (card.force) or (not already_downloaded(card)): + download_image(card) + mark_as_downloaded(card) sleep(.1) @@ -87,7 +45,7 @@ def main(): try: while True: - cards, is_artwork, force = handle_input( input(INPUT_STRING) ) + cards = handle_input( input(INPUT_STRING) ) # If couldn't find what to download if cards is None: @@ -97,8 +55,8 @@ def main(): total_cards = len(cards) # For each card, download - for index, card_id in enumerate(cards, 1): - to_download(card_id, is_artwork, force) + for index, card in enumerate(cards, 1): + to_download(card) # Prints progress raw_progress = f"{index}/{total_cards}" diff --git a/tracker.py b/tracker.py index 7c3dbfe..33f47d7 100644 --- a/tracker.py +++ b/tracker.py @@ -1,3 +1,6 @@ +from commands.typing import DownloadCard + + card_cache_path = "./hd_cards_downloader_tracker" field_cache_path = "./hd_fields_downloader_tracker" @@ -11,18 +14,18 @@ def __get_cached(is_artwork: bool): cache_file.close() return cards -def already_downloaded(card_id: int, is_artwork: bool = False): - """Returns True if card with id 'card_id' was +def already_downloaded(card: DownloadCard): + """Returns True if card with id 'card.card_id' was already downloaded""" - cards_downloaded = __get_cached(is_artwork) - return str(card_id) in cards_downloaded + cards_downloaded = __get_cached(card.artwork) + return str(card.card_id) in cards_downloaded -def mark_as_downloaded(card_id: int, is_artwork: bool = False): +def mark_as_downloaded(card: DownloadCard): """Opens tracker file to add an id to the downloaded list""" - cache = card_cache_path if not is_artwork else field_cache_path + cache = card_cache_path if not card.artwork else field_cache_path cache_file = open(cache, mode="a+", encoding="utf8") - cache_file.write(f"{card_id}\n") + cache_file.write(f"{card.card_id}\n") cache_file.close()