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()