diff --git a/app.py b/app.py index f90f00b..1479b64 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,10 @@ -""" +""" MODULES """ +# pylint: disable=R0912, R1702 import os import sys +import json from urllib import parse from colorama import Style, Fore import requests as req @@ -31,37 +33,41 @@ def txt_downloader(): # Have user choose the set, then download land_set = input("Basic land found! What set should I pull the art from? ex: vow, m21, eld\n") if len(land_set) >= 3: - c = req.get(f"https://api.scryfall.com/cards/named?fuzzy={parse.quote(card)}&set={parse.quote(land_set)}").json() - dl.Basic(c).download() - break - print("Error! Illegitimate set. Try again!\n") + try: + c = req.get(f"https://api.scryfall.com/cards/named?fuzzy={parse.quote(card)}&set={parse.quote(land_set)}").json() + dl.Basic(c).download() + break + except: print("Scryfall couldn't find this set. Try again!\n") + else: print("Error! Illegitimate set. Try again!\n") elif cfg.download_all: + try: + # Retrieve scryfall data + r = req.get(f"https://api.scryfall.com/cards/search?q=!\"{parse.quote(card)}\" is:hires&unique="+cfg.unique+"&order=released").json() - # Retrieve scryfall data - r = req.get(f"https://api.scryfall.com/cards/search?q=!\"{parse.quote(card)}\" is:hires&unique="+cfg.unique+"&order=released").json() - - # Loop through prints of this card - for c in r: - card_class = dl.get_card_class(c) - card_class(c).download() + # Loop through prints of this card + for c in r['data']: + card_class = dl.get_card_class(c) + card_class(c).download() + except: print(f"{card} not found!") else: - - # Retrieve scryfall data - r = req.get(f"https://api.scryfall.com/cards/search?q=!\"{parse.quote(card)}\" is:hires&unique="+cfg.unique+"&order=released").json() - - # Remove full art entries? - prepared = [] - if cfg.exclude_fullart: - for t in r['data']: - if t['full_art'] is False: prepared.append(t) - else: prepared = r['data'] - - # Loop through prints of this card - for c in prepared: - card_class = dl.get_card_class(c) - card_class(c).download() + try: + # Retrieve scryfall data + r = req.get(f"https://api.scryfall.com/cards/search?q=!\"{parse.quote(card)}\" is:hires&unique="+cfg.unique+"&order=released").json() + + # Remove full art entries? + prepared = [] + if cfg.exclude_fullart: + for t in r['data']: + if t['full_art'] is False: prepared.append(t) + else: prepared = r['data'] + + # Loop through prints of this card + for c in prepared: + card_class = dl.get_card_class(c) + card_class(c).download() + except: print(f"{card} not found!") def sheet_downloader(): """ @@ -78,17 +84,19 @@ def sheet_downloader(): name = card[1] # Lookup card - c = req.get(f"https://api.scryfall.com/cards/named?fuzzy={parse.quote(name)}&set={parse.quote(set_code)}").json() - card_class = dl.get_card_class(c) - card_class(c).download() + try: + c = req.get(f"https://api.scryfall.com/cards/named?fuzzy={parse.quote(name)}&set={parse.quote(set_code)}").json() + card_class = dl.get_card_class(c) + card_class(c).download() + except: print(f"{name} not found in set: {set_code}") print(f"{Fore.YELLOW}{Style.BRIGHT}\n") -print(" ██████╗ ███████╗████████╗ ███╗ ███╗████████╗ ██████╗ ") -print(" ██╔════╝ ██╔════╝╚══██╔══╝ ████╗ ████║╚══██╔══╝██╔════╝ ") -print(" ██║ ███╗█████╗ ██║ ██╔████╔██║ ██║ ██║ ███╗") -print(" ██║ ██║██╔══╝ ██║ ██║╚██╔╝██║ ██║ ██║ ██║") -print(" ╚██████╔╝███████╗ ██║ ██║ ╚═╝ ██║ ██║ ╚██████╔╝") -print(" ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ") +print(" ██████╗ ███████╗████████╗ ███╗ ███╗████████╗ ██████╗ ") +print(" ██╔════╝ ██╔════╝╚══██╔══╝ ████╗ ████║╚══██╔══╝██╔════╝ ") +print(" ██║ ███╗█████╗ ██║ ██╔████╔██║ ██║ ██║ ███╗") +print(" ██║ ██║██╔══╝ ██║ ██║╚██╔╝██║ ██║ ██║ ██║") +print(" ╚██████╔╝███████╗ ██║ ██║ ╚═╝ ██║ ██║ ╚██████╔╝") +print(" ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ") print(" █████╗ ██████╗ ████████╗ ███╗ ██╗ ██████╗ ██╗ ██╗ ") print(" ██╔══██╗██╔══██╗╚══██╔══╝ ████╗ ██║██╔═══██╗██║ ██║ ") print(" ███████║██████╔╝ ██║ ██╔██╗ ██║██║ ██║██║ █╗ ██║ ") diff --git a/detailed.txt b/detailed.txt index 55b1b85..a88ebed 100644 --- a/detailed.txt +++ b/detailed.txt @@ -1,280 +1,49 @@ -stx--Academic Dispute -stx--Academic Probation -stx--Access Tunnel -stx--Accomplished Alchemist -stx--Aether Helix -stx--Ageless Guardian -stx--Arcane Subtraction -stx--Archmage Emeritus -stx--Archway Commons -stx--Ardent Dustspeaker -stx--Arrogant Poet -stx--Augmenter Pugilist -stx--Baleful Mastery -stx--Basic Conjuration -stx--Bayou Groff -stx--Beaming Defiance -stx--Beledros Witherbloom -stx--Biblioplex Assistant -stx--Big Play -stx--Biomathematician -stx--Blade Historian -stx--Blex, Vexing Pest -stx--Blood Age General -stx--Blood Researcher -stx--Blot Out the Sky -stx--Body of Research -stx--Bookwurm -stx--Brackish Trudge -stx--Burrog Befuddler -stx--Bury in Books -stx--Callous Bloodmage -stx--Campus Guide -stx--Charge Through -stx--Clever Lumimancer -stx--Closing Statement -stx--Codie, Vociferous Codex -stx--Cogwork Archivist -stx--Combat Professor -stx--Confront the Past -stx--Conspiracy Theorist -stx--Containment Breach -stx--Crackle with Power -stx--Cram Session -stx--Creative Outburst -stx--Crushing Disappointment -stx--Culling Ritual -stx--Culmination of Studies -stx--Curate -stx--Daemogoth Titan -stx--Daemogoth Woe-Eater -stx--Deadly Brew -stx--Decisive Denial -stx--Defend the Campus -stx--Detention Vortex -stx--Devastating Mastery -stx--Devouring Tendrils -stx--Dina, Soul Steeper -stx--Divide by Zero -stx--Double Major -stx--Draconic Intervention -stx--Dragon's Approach -stx--Dragonsguard Elite -stx--Dramatic Finale -stx--Dream Strix -stx--Dueling Coach -stx--Eager First-Year -stx--Ecological Appreciation -stx--Efreet Flamepainter -stx--Elemental Expressionist -stx--Elemental Masterpiece -stx--Elemental Summoning -stx--Elite Spellbinder -stx--Emergent Sequence -stx--Enthusiastic Study -stx--Environmental Sciences -stx--Essence Infusion -stx--Eureka Moment -stx--Excavated Wall -stx--Exhilarating Elocution -stx--Expanded Anatomy -stx--Expel -stx--Explosive Welcome -stx--Exponential Growth -stx--Expressive Iteration -stx--Extus, Oriq Overlord -stx--Eyetwitch -stx--Fervent Mastery -stx--Field Trip -stx--First Day of Class -stx--Flamescroll Celebrant -stx--Flunk -stx--Forest -stx--Fortifying Draught -stx--Fractal Summoning -stx--Fracture -stx--Frostboil Snarl -stx--Frost Trickster -stx--Fuming Effigy -stx--Furycalm Snarl -stx--Galazeth Prismari -stx--Gnarled Professor -stx--Go Blank -stx--Golden Ratio -stx--Grinning Ignus -stx--Guiding Voice -stx--Hall Monitor -stx--Hall of Oracles -stx--Harness Infinity -stx--Heated Debate -stx--Hofri Ghostforge -stx--Honor Troll -stx--Humiliate -stx--Hunt for Specimens -stx--Igneous Inspiration -stx--Illuminate History -stx--Illustrious Historian -stx--Infuse with Vitality -stx--Ingenious Mastery -stx--Inkling Summoning -stx--Introduction to Annihilation -stx--Introduction to Prophecy -stx--Island -stx--Jadzi, Oracle of Arcavios -stx--Karok Wrangler -stx--Kasmina, Enigma Sage -stx--Kelpie Guide -stx--Kianne, Dean of Substance -stx--Killian, Ink Duelist -stx--Lash of Malice -stx--Leech Fanatic -stx--Leonin Lightscribe -stx--Letter of Acceptance -stx--Leyline Invocation -stx--Lorehold Apprentice -stx--Lorehold Campus -stx--Lorehold Command -stx--Lorehold Excavation -stx--Lorehold Pledgemage -stx--Maelstrom Muse -stx--Mage Duel -stx--Mage Hunter -stx--Mage Hunters' Onslaught -stx--Magma Opus -stx--Make Your Mark -stx--Manifestation Sage -stx--Mascot Exhibition -stx--Mascot Interception -stx--Master Symmetrist -stx--Mavinda, Students' Advocate -stx--Mentor's Guidance -stx--Mercurial Transformation -stx--Mila, Crafty Companion -stx--Moldering Karok -stx--Mortality Spear -stx--Mountain -stx--Multiple Choice -stx--Necroblossom Snarl -stx--Necrotic Fumes -stx--Needlethorn Drake -stx--Novice Dissector -stx--Oggyar Battle-Seer -stx--Oriq Loremage -stx--Overgrown Arch -stx--Owlin Shieldmage -stx--Pestilent Cauldron -stx--Pest Summoning -stx--Pigment Storm -stx--Pilgrim of the Ages -stx--Pillardrop Rescuer -stx--Pillardrop Warden -stx--Plains -stx--Plargg, Dean of Chaos -stx--Plumb the Forbidden -stx--Poet's Quill -stx--Pop Quiz -stx--Practical Research -stx--Prismari Apprentice -stx--Prismari Campus -stx--Prismari Command -stx--Prismari Pledgemage -stx--Professor of Symbology -stx--Professor of Zoomancy -stx--Professor Onyx -stx--Professor's Warning -stx--Promising Duskmage -stx--Quandrix Apprentice -stx--Quandrix Campus -stx--Quandrix Command -stx--Quandrix Cultivator -stx--Quandrix Pledgemage -stx--Quintorius, Field Historian -stx--Radiant Scrollwielder -stx--Reckless Amplimancer -stx--Reconstruct History -stx--Reduce to Memory -stx--Reflective Golem -stx--Reject -stx--Relic Sloth -stx--Resculpt -stx--Retriever Phoenix -stx--Returned Pastcaller -stx--Rip Apart -stx--Rise of Extus -stx--Rootha, Mercurial Artist -stx--Rowan, Scholar of Sparks -stx--Rushed Rebirth -stx--Scurrid Colony -stx--Secret Rendezvous -stx--Sedgemoor Witch -stx--Selfless Glyphweaver -stx--Semester's End -stx--Serpentine Curve -stx--Shadewing Laureate -stx--Shadrix Silverquill -stx--Shaile, Dean of Radiance -stx--Shineshadow Snarl -stx--Show of Confidence -stx--Silverquill Apprentice -stx--Silverquill Campus -stx--Silverquill Command -stx--Silverquill Pledgemage -stx--Silverquill Silencer -stx--Snow Day -stx--Solve the Equation -stx--Soothsayer Adept -stx--Sparring Regimen -stx--Spectacle Mage -stx--Specter of the Fens -stx--Spell Satchel -stx--Spined Karok -stx--Spirit Summoning -stx--Spiteful Squad -stx--Springmane Cervin -stx--Square Up -stx--Star Pupil -stx--Start from Scratch -stx--Stonebinder's Familiar -stx--Stonebound Mentor -stx--Stonerise Spirit -stx--Storm-Kiln Artist -stx--Strict Proctor -stx--Strixhaven Stadium -stx--Study Break -stx--Sudden Breakthrough -stx--Swamp -stx--Symmetry Sage -stx--Tanazir Quandrix -stx--Tangletrap -stx--Teach by Example -stx--Teachings of the Archaics -stx--Team Pennant -stx--Tempted by the Oriq -stx--Tend the Pests -stx--Tenured Inkcaster -stx--Test of Talents -stx--The Biblioplex -stx--Thrilling Discovery -stx--Thunderous Orator -stx--Tome Shredder -stx--Torrent Sculptor -stx--Twinscroll Shaman -stx--Umbral Juke -stx--Unwilling Ingredient -stx--Uvilda, Dean of Perfection -stx--Valentin, Dean of the Vein -stx--Vanishing Verse -stx--Velomachus Lorehold -stx--Venerable Warsinger -stx--Verdant Mastery -stx--Vineglimmer Snarl -stx--Vortex Runner -stx--Wandering Archaic -stx--Waterfall Aerialist -stx--Witherbloom Apprentice -stx--Witherbloom Campus -stx--Witherbloom Command -stx--Witherbloom Pledgemage -stx--Wormhole Serpent -stx--Zephyr Boots -stx--Zimone, Quandrix Prodigy \ No newline at end of file +mid--Ambitious Farmhand +mid--Arlinn, the Pack's Hope +mid--Baithook Angler +mid--Baneblade Scoundrel +mid--Beloved Beggar +mid--Bereaved Survivor +mid--Bird Admirer +mid--Brutal Cathar +mid--Burly Breaker +mid--Chaplain of Alms +mid--Covert Cutpurse +mid--Covetous Castaway +mid--Curse of Leeches +mid--Deathbonnet Sprout +mid--Delver of Secrets +mid--Dennick, Pious Apprentice +mid--Devoted Grafkeeper +mid--Ecstatic Awakener +mid--Enduring Angel +mid--Fangblade Brigand +mid--Flame Channeler +mid--Galedrifter +mid--Graveyard Trespasser +mid--Harvesttide Infiltrator +mid--Heirloom Mirror +mid--Hostile Hostel +mid--Hound Tamer +mid--Jerren, Corrupted Bishop +mid--Kessig Naturalist +mid--Ludevic, Necrogenius +mid--Lunarch Veteran +mid--Malevolent Hermit +mid--Mourning Patrol +mid--Mysterious Tome +mid--Mystic Skull +mid--Outland Liberator +mid--Overwhelmed Archivist +mid--Poppet Stitcher +mid--Reckless Stormseeker +mid--Shady Traveler +mid--Smoldering Egg +mid--Spellrune Painter +mid--Suspicious Stowaway +mid--Tavern Ruffian +mid--Tireless Hauler +mid--Tovolar, Dire Overlord +mid--Tovolar's Huntmaster +mid--Vengeful Strangler +mid--Village Watch \ No newline at end of file diff --git a/lib/card.py b/lib/card.py index beb58d5..a621693 100644 --- a/lib/card.py +++ b/lib/card.py @@ -69,13 +69,7 @@ def download_mtgp (self, name, path, mtgp_code, back=False): soup_img = soup.find_all("img", {"style": "display:block;border:4px black solid;cursor:pointer;"}) # Is this the back face? - if back: - img_src = soup_img[1]['src'] - name = name + " (Back)" - else: img_src = soup_img[0]['src'] - - # Final mtgp IMG link - img_link = img_src.replace("pics/art_th/","https://mtgpics.com/pics/art/") + img_link = core.get_card_face(soup_img, back) # Try to download from MTG Pics request.urlretrieve(img_link, path) @@ -109,7 +103,7 @@ def __init__ (self, c): self.scry_path = os.path.join(cwd, f"{cfg.scry}/{self.name} ({self.artist}) [{self.set.upper()}].jpg") -class Basic (Normal): +class Land (Normal): """ Basic land card """ @@ -118,12 +112,12 @@ def __init__ (self, c): # Saved filepath self.mtgp_path = os.path.join(cwd, - f"{cfg.mtgp}/Basic Land/{self.name} ({self.artist}) [{self.set.upper()}].jpg") + f"{cfg.mtgp}/Land/{self.name} ({self.artist}) [{self.set.upper()}].jpg") self.scry_path = os.path.join(cwd, - f"{cfg.scry}/Basic Land/{self.name} ({self.artist}) [{self.set.upper()}].jpg") + f"{cfg.scry}/Land/{self.name} ({self.artist}) [{self.set.upper()}].jpg") # Ensure save folder exists - super().make_folders("Basic Land") + super().make_folders("Land") class Saga (Normal): """ @@ -205,6 +199,38 @@ def __init__ (self, c): # Ensure save folder exists super().make_folders("Class") +class Flip (Normal): + """ + Flip card + """ + def __init__ (self, c): + super().__init__(c) + + # Saved filepath + self.mtgp_path = os.path.join(cwd, + f"{cfg.mtgp}/Flip/{self.name} ({self.artist}) [{self.set.upper()}].jpg") + self.scry_path = os.path.join(cwd, + f"{cfg.scry}/Flip/{self.name} ({self.artist}) [{self.set.upper()}].jpg") + + # Ensure save folder exists + super().make_folders("Flip") + +class Split (Normal): + """ + Split card + """ + def __init__ (self, c): + super().__init__(c) + + # Saved filepath + self.mtgp_path = os.path.join(cwd, + f"{cfg.mtgp}/Split/{self.name} ({self.artist}) [{self.set.upper()}].jpg") + self.scry_path = os.path.join(cwd, + f"{cfg.scry}/Split/{self.name} ({self.artist}) [{self.set.upper()}].jpg") + + # Ensure save folder exists + super().make_folders("Split") + class Planar (Normal): """ Planar card @@ -265,7 +291,7 @@ def download (self, log_failed=True): # Download back back = True - try: self.download_mtgp (self.name_back, self.mtgp_path_back, self.code, True) + try: self.download_mtgp (f"{self.name_back} (Back)", self.mtgp_path_back, self.code, True) except: if cfg.download_scryfall: super.download_scryfall (self.name_back, self.scry_path_back, self.scrylink_back) else: back = False @@ -310,7 +336,6 @@ def get_card_class(c): """ class_map = { "normal": Normal, - "basic": Basic, "transform": Transform, "modal_dfc": MDFC, "adventure": Adventure, @@ -318,12 +343,14 @@ def get_card_class(c): "saga": Saga, "planar": Planar, "meld": Meld, - "class": Class + "class": Class, + "split": Split, + "flip": Flip, } # Planeswalker? if "Planeswalker" in c['type_line'] and "card_faces" not in c: return Planeswalker - if c['name'] in cfg.basic_lands: - return Basic + if "Land" in c['type_line'] and "card_faces" not in c: + return Land return class_map[c['layout']] diff --git a/lib/core.py b/lib/core.py index dacc556..bc192eb 100644 --- a/lib/core.py +++ b/lib/core.py @@ -1,7 +1,7 @@ """ CORE FUNCTIONS """ -# pylint: disable=E0401 +# pylint: disable=E0401, R0911, R0912 import os import re import sys @@ -53,6 +53,68 @@ def handle (error): print(f"{error}\nPress enter to exit...") sys.exit() +def get_card_face(entries, back): + """ + Determine which images are back and front + """ + arr = [] + path = f"https://mtgpics.com/{os.path.dirname(entries[0]['src'])}" + path = path.replace("art_th","art") + + for i in entries: + arr.append(os.path.basename(i['src']).replace(".jpg","")) + if len(arr) == 1: + if back: return None + return f"{path}/{arr[0]}.jpg" + if len(arr) == 2: + # Two sides exist + try: + # Bigger number usually the back + if int(arr[0]) > int(arr[1]): + if back: return f"{path}/{arr[0]}.jpg" + return f"{path}/{arr[1]}.jpg" + if int(arr[1]) > int(arr[0]): + if back: return f"{path}/{arr[1]}.jpg" + return f"{path}/{arr[0]}.jpg" + except: + try: + # In some cases backs have longer string + if len(arr[0]) > len(arr[1]): + if back: return f"{path}/{arr[0]}.jpg" + return f"{path}/{arr[1]}.jpg" + if len(arr[1]) > len(arr[0]): + if back: return f"{path}/{arr[1]}.jpg" + return f"{path}/{arr[0]}.jpg" + except: pass + # All other cases just go in order + if back: return f"{path}/{arr[1]}.jpg" + return f"{path}/{arr[0]}.jpg" + if len(arr) > 2: + img_i = [] + img_s = [] + # Separate into string array and int array, sorted + arr.sort() + for i in arr: + if len(i) == 3: img_i.append(i) + elif len(i) > 3: img_s.append(i) + + # Try comparing ints + if len(img_i) > 1: + if back: return f"{path}/{img_i[1]}.jpg" + return f"{path}/{img_i[0]}.jpg" + # Try comparing strings + if len(img_s) > 1: + if back: return f"{path}/{img_s[1]}.jpg" + return f"{path}/{img_s[0]}.jpg" + # Or just go in order + if back: return f"{path}/{img_i[0]}.jpg" + return f"{path}/{img_s[0]}.jpg" + + # All else failed go in order + if back: return f"{path}/{arr[1]}.jpg" + return f"{path}/{arr[0]}.jpg" + + def fix_mtgp_set (set_code): """ Replace the real set code with MTGP's weird code