From 6af6a0c86f78415ad60a5a4dfc67db6f654a49e3 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Wed, 17 Feb 2021 01:11:33 -0500 Subject: [PATCH 01/17] Adding Scryfall call to look up two, two, two spells in one! --- src/replacements.config | 31 +---------------------- src/tcg-to-deckbox.py | 55 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/replacements.config b/src/replacements.config index a4c9ef7..816ea1f 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -45,6 +45,7 @@ Magic 2012 (M12)=Magic 2012 Magic 2013 (M13)=Magic 2013 Magic 2014 (M14)=Magic 2014 Core Set Magic 2015 (M15)=Magic 2015 Core Set +Game Day & Store Championship Promos=Magic Game Day Cards Media Promos=Media Inserts Magic Modern Event Deck=Modern Event Deck 2014 Modern Masters 2015=Modern Masters 2015 Edition @@ -69,36 +70,6 @@ World Championship Decks=Worlds WPN & Gateway Promos=WPN/Gateway [NAMES] -Animating Faerie=Animating Faerie // Bring to Life -Ardenvale Tactician=Ardenvale Tactician // Dizzying Swoop -Beanstalk Giant=Beanstalk Giant // Fertile Footsteps -Bonecrusher Giant=Bonecrusher Giant // Stomp -Brazen Borrower=Brazen Borrower // Petty Theft -Curious Pair=Curious Pair // Treats to Share -Embereth Shieldbreaker=Embereth Shieldbreaker // Battle Display -Fae of Wishes=Fae of Wishes // Granted -Faerie Guidemother=Faerie Guidemother // Gift of the Fae -Flaxen Intruder=Flaxen Intruder // Welcome Home -Foulmire Knight=Foulmire Knight // Profane Insight -Garenbrig Carver=Garenbrig Carver // Shield's Might -Giant Killer=Giant Killer // Chop Down -Hypnotic Sprite=Hypnotic Sprite // Mesmeric Glare -Lonesome Unicorn=Lonesome Unicorn // Rider in Need -Lovestruck Beast=Lovestruck Beast // Heart's Desire -Merchant of the Vale=Merchant of the Vale // Haggle -Merfolk Secretkeeper=Merfolk Secretkeeper // Venture Deeper -Murderous Rider=Murderous Rider // Swift End -Oakhame Ranger=Oakhame Ranger // Bring Back -Order of Midnight=Order of Midnight // Alter Fate -Queen of Ice=Queen of Ice // Rage of Winter -Realm-Cloaked Giant=Realm-Cloaked Giant // Cast Off -Reaper of Night=Reaper of Night // Harvest Fear -Rimrock Knight=Rimrock Knight // Boulder Rush -Rosethorn Acolyte=Rosethorn Acolyte // Seasonal Ritual -Shepherd of the Flock=Shepherd of the Flock // Usher to Safety -Silverflame Squire=Silverflame Squire // On Alert -Smitten Swordmaster=Smitten Swordmaster // Curry Favor -Tuinvale Treefolk=Tuinvale Treefolk // Oaken Boon Tamiyo's Journal (Entry 922)=Tamiyo's Journal Tamiyo's Journal (Entry 711)=Tamiyo's Journal Tamiyo's Journal (Entry 546)=Tamiyo's Journal diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index a789a87..c897b58 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -1,16 +1,38 @@ import sys import csv -import os +import time +import os, os.path import configparser import re from tkinter import filedialog from tkinter import messagebox import tkinter as tk +import urllib.request, json + + +# Constants +MULTI_NAMES_FILE = "multiple_names.json" +SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1" + +#global vars +scryfall_data={} + + # Get rid of the root TK window, we don't need it. root = tk.Tk() root.withdraw() +def fetch_multiple_names(uri,page=1): + print('Begin: Download %s, page %s of results' % (uri, page) ) + with urllib.request.urlopen(uri) as url: + tmp_scryfall_data = json.loads(url.read().decode()) + + for x in tmp_scryfall_data["data"]: + scryfall_data[x["card_faces"][0]["name"]] = x["name"] + if "next_page" in tmp_scryfall_data: + fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) + # Utility function to replace strings in the csv from the replacements.config file. def replace_strings(dict, replacementSection, columnName): if dict[columnName].lower() in configParser[replacementSection].keys(): @@ -25,6 +47,32 @@ def getPathPrefix(): prefix = os.path.abspath(".") return prefix +#Check to see if we have DFC/Split/etc card names from scryfall and if it is up to date +try: + multi_files_last_updated = os.path.getmtime(MULTI_NAMES_FILE) + print("%s last modified: %s" % (MULTI_NAMES_FILE,time.ctime(multi_files_last_updated))) + now = time.time() + last_week = now - 60*60*24*7 + if multi_files_last_updated < last_week: + print("File %s is stale - updating..." % (MULTI_NAMES_FILE)) + fetch_multiple_names(SCRYFALL_URL) + with open(MULTI_NAMES_FILE, 'w') as multiple_names: + json.dump(scryfall_data, multiple_names) + print("Done!") + else: + print("Using existing %s file..." % MULTI_NAMES_FILE) + with open(MULTI_NAMES_FILE) as multiple_names: + scryfall_data = json.load(multiple_names) + print("Done!") + +except Exception: + print("File %s not found - creating..." % (MULTI_NAMES_FILE)) + fetch_multiple_names(SCRYFALL_URL) + with open(MULTI_NAMES_FILE, 'w') as multiple_names: + json.dump(scryfall_data, multiple_names) + print("Done!") + + # Get our input GUI=False @@ -88,6 +136,8 @@ def getPathPrefix(): row["Name"]=row["Name"].replace(" (Extended Art)","") row["Name"]=row["Name"].replace(" (Showcase)","") row["Name"]=row["Name"].replace(" (Borderless)","") + row["Name"]=row["Name"].replace(" (Stained Glass)","") + row["Name"]=row["Name"].replace(" (Etched Foil)","") #For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"]=row["Name"].replace(" - Full Art","") @@ -100,6 +150,9 @@ def getPathPrefix(): # Remove numbers, mostly for lands, but for some other special cases (M21 Teferi) row["Name"] = re.sub(r" \(\d+\)", "", row["Name"]) replace_strings(row, "NAMES", "Name") + if row["Name"] in scryfall_data: + row["Name"]=scryfall_data[row["Name"]] + # remove weird symbols from card numbers row["Card Number"] = re.sub(r"[*★]", "", row["Card Number"]) From 8f7bb6a5bc7fc52b0779e60c6efa84a799515d98 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Wed, 17 Feb 2021 01:23:06 -0500 Subject: [PATCH 02/17] Some linting fixes - still a ways to go --- src/tcg-to-deckbox.py | 101 +++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index c897b58..3abbc8b 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -1,13 +1,15 @@ import sys import csv import time -import os, os.path +import os +import os.path import configparser import re from tkinter import filedialog from tkinter import messagebox import tkinter as tk -import urllib.request, json +import urllib.request +import json # Constants @@ -15,28 +17,31 @@ SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1" #global vars -scryfall_data={} - +scryfall_data = {} # Get rid of the root TK window, we don't need it. root = tk.Tk() root.withdraw() -def fetch_multiple_names(uri,page=1): - print('Begin: Download %s, page %s of results' % (uri, page) ) + +def fetch_multiple_names(uri, page=1): + print('Begin: Download %s, page %s of results' % (uri, page)) with urllib.request.urlopen(uri) as url: tmp_scryfall_data = json.loads(url.read().decode()) - + for x in tmp_scryfall_data["data"]: - scryfall_data[x["card_faces"][0]["name"]] = x["name"] + scryfall_data[x["card_faces"][0]["name"]] = x["name"] if "next_page" in tmp_scryfall_data: fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) # Utility function to replace strings in the csv from the replacements.config file. + + def replace_strings(dict, replacementSection, columnName): if dict[columnName].lower() in configParser[replacementSection].keys(): - dict[columnName]=configParser[replacementSection][dict[columnName].lower()] + dict[columnName] = configParser[replacementSection][dict[columnName].lower()] + def getPathPrefix(): try: @@ -47,10 +52,12 @@ def getPathPrefix(): prefix = os.path.abspath(".") return prefix -#Check to see if we have DFC/Split/etc card names from scryfall and if it is up to date + +# Check to see if we have DFC/Split/etc card names from scryfall and if it is up to date try: multi_files_last_updated = os.path.getmtime(MULTI_NAMES_FILE) - print("%s last modified: %s" % (MULTI_NAMES_FILE,time.ctime(multi_files_last_updated))) + print("%s last modified: %s" % + (MULTI_NAMES_FILE, time.ctime(multi_files_last_updated))) now = time.time() last_week = now - 60*60*24*7 if multi_files_last_updated < last_week: @@ -59,7 +66,7 @@ def getPathPrefix(): with open(MULTI_NAMES_FILE, 'w') as multiple_names: json.dump(scryfall_data, multiple_names) print("Done!") - else: + else: print("Using existing %s file..." % MULTI_NAMES_FILE) with open(MULTI_NAMES_FILE) as multiple_names: scryfall_data = json.load(multiple_names) @@ -73,32 +80,35 @@ def getPathPrefix(): print("Done!") - # Get our input -GUI=False +GUI = False if len(sys.argv) < 2: - GUI=True - FILE=filedialog.askopenfilename(title="Select your TCGPlayer app export file",filetypes=[("TCGPlayer exports", ".csv"),("All files","*.*")]) + GUI = True + FILE = filedialog.askopenfilename(title="Select your TCGPlayer app export file", filetypes=[ + ("TCGPlayer exports", ".csv"), ("All files", "*.*")]) if len(FILE) == 0: - messagebox.showerror(title="Input file not provided",message="You must pass the TCGPlayer csv export file to this program.") + messagebox.showerror(title="Input file not provided", + message="You must pass the TCGPlayer csv export file to this program.") sys.exit() else: - FILE=sys.argv[1] + FILE = sys.argv[1] -skipcolumns=["Simple Name","Set Code","Rarity","Product ID","SKU","Price","Price Each"] -outputFile="deckbox_import.csv" +skipcolumns = ["Simple Name", "Set Code", "Rarity", + "Product ID", "SKU", "Price", "Price Each"] +outputFile = "deckbox_import.csv" configParser = configparser.ConfigParser(delimiters="=") -configParser.read(os.path.join(getPathPrefix(),"replacements.config")) +configParser.read(os.path.join(getPathPrefix(), "replacements.config")) -with open(FILE, newline="") as tcgcsvfile,open(outputFile, "w", newline="") as deckboxcsvfile: +with open(FILE, newline="") as tcgcsvfile, open(outputFile, "w", newline="") as deckboxcsvfile: try: - csv.Sniffer().sniff(tcgcsvfile.read(4096),delimiters=",") + csv.Sniffer().sniff(tcgcsvfile.read(4096), delimiters=",") tcgcsvfile.seek(0) except: if GUI: - messagebox.showerror(title="Invalid input file",message="The file selected does not appear to be a valid CSV file.") + messagebox.showerror( + title="Invalid input file", message="The file selected does not appear to be a valid CSV file.") else: print("The file passed does not appear to be a valid CSV file.") sys.exit() @@ -109,21 +119,22 @@ def getPathPrefix(): headerstcg = csvreader.fieldnames for index, header in enumerate(headerstcg): if header.lower() in configParser["COLUMNS"].keys(): - headerstcg[index]=configParser["COLUMNS"][header.lower()] + headerstcg[index] = configParser["COLUMNS"][header.lower()] # Unnecessary Columns: Simple Name,Set Code,Printing,Rarity,Product ID,SKU,Price,Price Each. - headersdeckbox=[x for x in headerstcg if x not in skipcolumns] + headersdeckbox = [x for x in headerstcg if x not in skipcolumns] - csvwriter = csv.DictWriter(deckboxcsvfile, quoting=csv.QUOTE_ALL, fieldnames=headersdeckbox) + csvwriter = csv.DictWriter( + deckboxcsvfile, quoting=csv.QUOTE_ALL, fieldnames=headersdeckbox) csvwriter.writeheader() for row in csvreader: # Don't bother with columns that are going to be ignored anyways for skippable in skipcolumns: - row.pop(skippable,'') + row.pop(skippable, '') # Map the printing column to the Foil column if row["Foil"] == "Normal": - row["Foil"]="" + row["Foil"] = "" # Map Card Condition replace_strings(row, "CONDITIONS", "Condition") @@ -132,27 +143,25 @@ def getPathPrefix(): replace_strings(row, "LANGUAGES", "Language") # Map Specific Card Names, and drop extra tidbits - row["Name"]=row["Name"].replace(" (Alternate Art)","") - row["Name"]=row["Name"].replace(" (Extended Art)","") - row["Name"]=row["Name"].replace(" (Showcase)","") - row["Name"]=row["Name"].replace(" (Borderless)","") - row["Name"]=row["Name"].replace(" (Stained Glass)","") - row["Name"]=row["Name"].replace(" (Etched Foil)","") - #For BFZ lands...there's no differentiator from the full arts and the non full arts. - row["Name"]=row["Name"].replace(" - Full Art","") - - #Very specifc conditons + row["Name"] = row["Name"].replace(" (Alternate Art)", "") + row["Name"] = row["Name"].replace(" (Extended Art)", "") + row["Name"] = row["Name"].replace(" (Showcase)", "") + row["Name"] = row["Name"].replace(" (Borderless)", "") + row["Name"] = row["Name"].replace(" (Stained Glass)", "") + row["Name"] = row["Name"].replace(" (Etched Foil)", "") + # For BFZ lands...there's no differentiator from the full arts and the non full arts. + row["Name"] = row["Name"].replace(" - Full Art", "") + + # Very specifc conditons if row["Name"].contains("(JP Alternate Art)") and row["Edition"] == "War of the Spark": row["Edition"] = "War of the Spark Japanese Alternate Art" - row["Name"]=row["Name"].replace(" (JP Alternate Art)","") - + row["Name"] = row["Name"].replace(" (JP Alternate Art)", "") # Remove numbers, mostly for lands, but for some other special cases (M21 Teferi) row["Name"] = re.sub(r" \(\d+\)", "", row["Name"]) replace_strings(row, "NAMES", "Name") if row["Name"] in scryfall_data: - row["Name"]=scryfall_data[row["Name"]] - + row["Name"] = scryfall_data[row["Name"]] # remove weird symbols from card numbers row["Card Number"] = re.sub(r"[*★]", "", row["Card Number"]) @@ -164,8 +173,10 @@ def getPathPrefix(): csvwriter.writerow(row) # All Done! -successMsg="Your import file for deckbox.org is available here: %s" % os.path.abspath(outputFile) +successMsg = "Your import file for deckbox.org is available here: %s" % os.path.abspath( + outputFile) if GUI: - messagebox.showinfo(title="Conversion completed successfully!", message=successMsg) + messagebox.showinfo( + title="Conversion completed successfully!", message=successMsg) else: print(successMsg) From 33aa1d414d9f4b630ab20560e40c5b2b5ec3ef86 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Mon, 22 Feb 2021 00:42:50 -0500 Subject: [PATCH 03/17] attempting to fix ssl cert issue --- .gitignore | 4 ++++ src/tcg-to-deckbox.py | 17 +++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 8d01d5b..6ecb909 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ deckbox_import.csv +build +dist +multiple_names.json +__pycache__ diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 3abbc8b..f4e9c61 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -9,6 +9,7 @@ from tkinter import messagebox import tkinter as tk import urllib.request +import ssl import json @@ -27,13 +28,17 @@ def fetch_multiple_names(uri, page=1): print('Begin: Download %s, page %s of results' % (uri, page)) - with urllib.request.urlopen(uri) as url: - tmp_scryfall_data = json.loads(url.read().decode()) + try: + with urllib.request.urlopen(uri, context=ssl.create_default_context()) as url: + tmp_scryfall_data = json.loads(url.read().decode()) - for x in tmp_scryfall_data["data"]: - scryfall_data[x["card_faces"][0]["name"]] = x["name"] - if "next_page" in tmp_scryfall_data: - fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) + for x in tmp_scryfall_data["data"]: + scryfall_data[x["card_faces"][0]["name"]] = x["name"] + if "next_page" in tmp_scryfall_data: + fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) + except Exception: + print('Exception: Was unable to download %s, page %s of results' % (uri, page)) + print(Exception) # Utility function to replace strings in the csv from the replacements.config file. From 7934621a856036dcb623eaed833ea49d94deb522 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Sat, 27 Feb 2021 23:12:29 -0500 Subject: [PATCH 04/17] Python doesn't do string contains like Java Does --- src/tcg-to-deckbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index f4e9c61..6698e91 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -158,7 +158,7 @@ def getPathPrefix(): row["Name"] = row["Name"].replace(" - Full Art", "") # Very specifc conditons - if row["Name"].contains("(JP Alternate Art)") and row["Edition"] == "War of the Spark": + if "(JP Alternate Art)" in row["Name"] and row["Edition"] == "War of the Spark": row["Edition"] = "War of the Spark Japanese Alternate Art" row["Name"] = row["Name"].replace(" (JP Alternate Art)", "") From 12ff16257b8d8b157bfdacb9193752aa8dea6690 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Sat, 27 Feb 2021 23:28:56 -0500 Subject: [PATCH 05/17] switching to requests library --- src/tcg-to-deckbox.py | 6 +++--- src/tcg-to-deckbox.spec | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 6698e91..e05d5f0 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -8,7 +8,7 @@ from tkinter import filedialog from tkinter import messagebox import tkinter as tk -import urllib.request +import requests import ssl import json @@ -29,8 +29,8 @@ def fetch_multiple_names(uri, page=1): print('Begin: Download %s, page %s of results' % (uri, page)) try: - with urllib.request.urlopen(uri, context=ssl.create_default_context()) as url: - tmp_scryfall_data = json.loads(url.read().decode()) + with requests.get(uri) as response: + tmp_scryfall_data = response.json() for x in tmp_scryfall_data["data"]: scryfall_data[x["card_faces"][0]["name"]] = x["name"] diff --git a/src/tcg-to-deckbox.spec b/src/tcg-to-deckbox.spec index 597cda3..ed9abd9 100644 --- a/src/tcg-to-deckbox.spec +++ b/src/tcg-to-deckbox.spec @@ -8,7 +8,7 @@ a = Analysis(['tcg-to-deckbox.py'], binaries=[], datas=[('replacements.config', '.')], hiddenimports=['tkinter'], - hookspath=[], + hookspath=['hooks'], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, From 18beeb41c03488789d0fa9a40985ebca6dfff0a7 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Mon, 5 Jul 2021 22:54:30 -0400 Subject: [PATCH 06/17] adding stuff for newest entries --- src/replacements.config | 2 ++ src/tcg-to-deckbox.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/replacements.config b/src/replacements.config index 816ea1f..8d7d98c 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -68,6 +68,8 @@ Mythic Edition: War of the Spark=War of the Spark Mythic Edition WMCQ Promo Cards=World Magic Cup Qualifiers World Championship Decks=Worlds WPN & Gateway Promos=WPN/Gateway +Strixhaven: Mystical Archives=Strixhaven Mystical Archive +Time Spiral: Remastered=Time Spiral Remastered [NAMES] Tamiyo's Journal (Entry 922)=Tamiyo's Journal diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index e05d5f0..2faadb9 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -154,6 +154,7 @@ def getPathPrefix(): row["Name"] = row["Name"].replace(" (Borderless)", "") row["Name"] = row["Name"].replace(" (Stained Glass)", "") row["Name"] = row["Name"].replace(" (Etched Foil)", "") + row["Name"] = row["Name"].replace(" (Foil Etched)", "") # For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"] = row["Name"].replace(" - Full Art", "") From caa6f093c5ac7d54ebf04dad18faeb1a8d36ae80 Mon Sep 17 00:00:00 2001 From: Robert Konell Date: Wed, 29 Dec 2021 23:44:24 -0800 Subject: [PATCH 07/17] Added new replacement rules, added ignore rules for dualfaced cards that deckbox uses the front face name, removed art cards from scryfall query --- src/replacements.config | 4 ++++ src/tcg-to-deckbox.py | 26 ++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/replacements.config b/src/replacements.config index 8d7d98c..80100df 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -70,6 +70,10 @@ World Championship Decks=Worlds WPN & Gateway Promos=WPN/Gateway Strixhaven: Mystical Archives=Strixhaven Mystical Archive Time Spiral: Remastered=Time Spiral Remastered +Promo Pack: Strixhaven=Promo pack: Strixhaven: School of Mages +Commander: Adventures in the Forgotten Realms=Adventures in the Forgotten Realms Commander +Commander: Innistrad: Midnight Hunt=Innistrad: Midnight Hunt Commander +Commander: Innistrad: Crimson Vow=Innistrad: Crimson Vow Commander [NAMES] Tamiyo's Journal (Entry 922)=Tamiyo's Journal diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 2faadb9..140d2e9 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -15,7 +15,9 @@ # Constants MULTI_NAMES_FILE = "multiple_names.json" -SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1" +SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" +# some dual faced cards are referenced using only the front name on deckbox +MULTI_NAMES_IGNORE = ["Nicol Bolas, the Ravager","Hadana's Climb"] #global vars scryfall_data = {} @@ -33,7 +35,11 @@ def fetch_multiple_names(uri, page=1): tmp_scryfall_data = response.json() for x in tmp_scryfall_data["data"]: - scryfall_data[x["card_faces"][0]["name"]] = x["name"] + if x["card_faces"][0]["name"] in MULTI_NAMES_IGNORE: + # detected a card we want to ignore from scryfall + continue + else: + scryfall_data[x["card_faces"][0]["name"]] = x["name"] if "next_page" in tmp_scryfall_data: fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) except Exception: @@ -155,6 +161,22 @@ def getPathPrefix(): row["Name"] = row["Name"].replace(" (Stained Glass)", "") row["Name"] = row["Name"].replace(" (Etched Foil)", "") row["Name"] = row["Name"].replace(" (Foil Etched)", "") + row["Name"] = row["Name"].replace(" (Dungeon Module)", "") + row["Name"] = row["Name"].replace(" (CHAMPS)", "") + row["Name"] = row["Name"].replace(" (JP Alternate Art)", "") + row["Name"] = row["Name"].replace(" (No PW Symbol)", "") + row["Name"] = row["Name"].replace(" (Retro Frame)", "") + row["Name"] = row["Name"].replace(" (Phyrexian)", "") + row["Name"] = row["Name"].replace(" (Bring a Friend Promo)", "") + + # alternative art/name + row["Name"] = row["Name"].replace("Sisters of the Undead - ", "") + row["Name"] = row["Name"].replace("Mina Harker - ", "") + row["Name"] = row["Name"].replace("Abraham Van Helsing - ", "") + row["Name"] = row["Name"].replace("Dracula, Lord of Blood - ", "") + row["Name"] = row["Name"].replace("Dracula the Voyager - ", "") + row["Name"] = row["Name"].replace("Dracula, Blood Immortal - ", "") + # For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"] = row["Name"].replace(" - Full Art", "") From 71a7e757b76a33d96eb92a59f1b699aa783e2838 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Thu, 30 Dec 2021 23:16:40 -0500 Subject: [PATCH 08/17] checking in changes that don't work, before I forget --- .github/workflows/build.yml | 44 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 8 ++----- src/replacements.config | 1 + src/tcg-to-deckbox.py | 37 ++++++++++++++++++----------- 4 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4493c21 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,44 @@ +name: CI + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +# additionally allows a manual trigger via workflow_dispatch +on: + workflow_dispatch: + types: ManualUpdate + push: + branches: [ master ] + pull_request: + branches: [ master ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Build Linux Executable + uses: JackMcKew/pyinstaller-action-linux@tkinter + with: + path: src + tkinter: true + - name: Upload Linux Executable + uses: actions/upload-artifact@v2 + with: + name: tcg-to-deckbox + path: src/dist/linux + - name: Build Windows Executable + uses: JackMcKew/pyinstaller-action-windows@main + with: + path: src + - name: Upload Windows Executable + uses: actions/upload-artifact@v2 + with: + name: tcg-to-deckbox + path: src/dist/windows diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 054ccf8..45b04c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,4 @@ -# This is a basic workflow to help you get started with Actions - -name: CI +name: CD # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch @@ -10,8 +8,6 @@ on: types: ManualUpdate push: branches: [ master ] - pull_request: - branches: [ master ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -24,7 +20,7 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - + - name: Build Linux Executable uses: JackMcKew/pyinstaller-action-linux@tkinter with: diff --git a/src/replacements.config b/src/replacements.config index 8d7d98c..4607d1c 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -68,6 +68,7 @@ Mythic Edition: War of the Spark=War of the Spark Mythic Edition WMCQ Promo Cards=World Magic Cup Qualifiers World Championship Decks=Worlds WPN & Gateway Promos=WPN/Gateway +Promo Pack: Strixhaven=Promo Pack: Strixhaven: School of Mages Strixhaven: Mystical Archives=Strixhaven Mystical Archive Time Spiral: Remastered=Time Spiral Remastered diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 2faadb9..fdddcbb 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -15,11 +15,14 @@ # Constants MULTI_NAMES_FILE = "multiple_names.json" -SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1" +SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-layout%3Aart_series" #global vars scryfall_data = {} +#Global replacement helpers +ixalan_bab = ["Legion's Landing", "Search for Azcanta", "Arguel's Blood Fast", "Vance's Blasting Cannons", "Growing Rites of Itlimoc", "Conqueror's Galleon", "Dowsing Dagger", "Primal Amulet", "Thaumatic Compass", "Treasure Map"] + # Get rid of the root TK window, we don't need it. root = tk.Tk() @@ -133,6 +136,8 @@ def getPathPrefix(): deckboxcsvfile, quoting=csv.QUOTE_ALL, fieldnames=headersdeckbox) csvwriter.writeheader() for row in csvreader: + skip_scryfall_names=False + # Don't bother with columns that are going to be ignored anyways for skippable in skipcolumns: row.pop(skippable, '') @@ -148,25 +153,31 @@ def getPathPrefix(): replace_strings(row, "LANGUAGES", "Language") # Map Specific Card Names, and drop extra tidbits - row["Name"] = row["Name"].replace(" (Alternate Art)", "") - row["Name"] = row["Name"].replace(" (Extended Art)", "") - row["Name"] = row["Name"].replace(" (Showcase)", "") - row["Name"] = row["Name"].replace(" (Borderless)", "") - row["Name"] = row["Name"].replace(" (Stained Glass)", "") - row["Name"] = row["Name"].replace(" (Etched Foil)", "") - row["Name"] = row["Name"].replace(" (Foil Etched)", "") - # For BFZ lands...there's no differentiator from the full arts and the non full arts. - row["Name"] = row["Name"].replace(" - Full Art", "") + #row["Name"] = row["Name"].replace(" (Alternate Art)", "") + #row["Name"] = row["Name"].replace(" (Extended Art)", "") + #row["Name"] = row["Name"].replace(" (Showcase)", "") + #row["Name"] = row["Name"].replace(" (Borderless)", "") + #row["Name"] = row["Name"].replace(" (Stained Glass)", "") + #row["Name"] = row["Name"].replace(" (Etched Foil)", "") + #row["Name"] = row["Name"].replace(" (Foil Etched)", "") # Very specifc conditons + # war of the spark Alternate arts handled differently if "(JP Alternate Art)" in row["Name"] and row["Edition"] == "War of the Spark": row["Edition"] = "War of the Spark Japanese Alternate Art" row["Name"] = row["Name"].replace(" (JP Alternate Art)", "") + # Buy a Box Promos worled a little differently with Ixalan + if row["Name"] in ixalan_bab and row["Edition"] == "Buy-A-Box Promos": + row["Edition"] = "Black Friday Treasure Chest Promos" + skip_scryfall_names=True + + + # For BFZ lands...there's no differentiator from the full arts and the non full arts. + row["Name"] = row["Name"].replace(" - Full Art", "") - # Remove numbers, mostly for lands, but for some other special cases (M21 Teferi) - row["Name"] = re.sub(r" \(\d+\)", "", row["Name"]) + row["Name"] = re.sub(r" \([^)(]+\)$", "", row["Name"]) replace_strings(row, "NAMES", "Name") - if row["Name"] in scryfall_data: + if skip_scryfall_names == False and row["Name"] in scryfall_data: row["Name"] = scryfall_data[row["Name"]] # remove weird symbols from card numbers From 61e56cd9effe79e2aa39c75fd16437d1f63f4651 Mon Sep 17 00:00:00 2001 From: Robert Konell Date: Sun, 2 Jan 2022 23:25:44 -0800 Subject: [PATCH 09/17] more dual face cards to ignore from scryfall, added mapping for bab cards, updated regex for cases with multiple groups of parenthesis --- src/replacements.config | 1 - src/tcg-to-deckbox.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/replacements.config b/src/replacements.config index 3bd91db..8ca697e 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -71,7 +71,6 @@ WPN & Gateway Promos=WPN/Gateway Promo Pack: Strixhaven=Promo Pack: Strixhaven: School of Mages Strixhaven: Mystical Archives=Strixhaven Mystical Archive Time Spiral: Remastered=Time Spiral Remastered -Promo Pack: Strixhaven=Promo pack: Strixhaven: School of Mages Commander: Adventures in the Forgotten Realms=Adventures in the Forgotten Realms Commander Commander: Innistrad: Midnight Hunt=Innistrad: Midnight Hunt Commander Commander: Innistrad: Crimson Vow=Innistrad: Crimson Vow Commander diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 9514906..d25d1d0 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -16,7 +16,7 @@ # Constants MULTI_NAMES_FILE = "multiple_names.json" SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" -MULTI_NAMES_IGNORE = ["Nicol Bolas, the Ravager","Hadana's Climb"] +MULTI_NAMES_IGNORE = ["Nicol Bolas, the Ravager","Hadana's Climb", "Treasure Map", "Storm the Vault"] #global vars scryfall_data = {} @@ -24,6 +24,9 @@ #Global replacement helpers ixalan_bab = ["Legion's Landing", "Search for Azcanta", "Arguel's Blood Fast", "Vance's Blasting Cannons", "Growing Rites of Itlimoc", "Conqueror's Galleon", "Dowsing Dagger", "Primal Amulet", "Thaumatic Compass", "Treasure Map"] +bab_mapping = { + "Impervious Greatwurm": "Guilds of Ravnica" +} # Get rid of the root TK window, we don't need it. root = tk.Tk() @@ -178,12 +181,14 @@ def getPathPrefix(): if row["Name"] in ixalan_bab and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = "Black Friday Treasure Chest Promos" skip_scryfall_names=True - + # Handle other BaB promo cards + if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": + row["Edition"] = bab_mapping[row["Name"]] # For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"] = row["Name"].replace(" - Full Art", "") - row["Name"] = re.sub(r" \([^)(]+\)$", "", row["Name"]) + row["Name"] = re.sub(r" \(.*\)", "", row["Name"]) replace_strings(row, "NAMES", "Name") if skip_scryfall_names == False and row["Name"] in scryfall_data: row["Name"] = scryfall_data[row["Name"]] From 83da1adb0e5ccf407061d0da98d2f8be7ee41559 Mon Sep 17 00:00:00 2001 From: Robert Konell Date: Tue, 4 Jan 2022 16:41:12 -0800 Subject: [PATCH 10/17] needed to update how mystery booster test cards are handled -- deckbox and tcgplay use different columns to differentiate between the original release and the 2021 release. physically the 2021 release does not include a planeswalker symbol in the lower left (and is much less valued sadly) --- src/tcg-to-deckbox.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index d25d1d0..1d137bc 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -184,6 +184,10 @@ def getPathPrefix(): # Handle other BaB promo cards if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = bab_mapping[row["Name"]] + # Handle Mystery Booster Test Cards, the 2021 release differentiates by Edition + # on deckbox, while tcgplayer differentiates by name appending '(No PW Symbol)' + if "(No PW Symbol)" in row["Name"] and row["Edition"] == "Mystery Booster: Convention Edition Exclusives": + row["Edition"] = "Mystery Booster Playtest Cards 2021" # For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"] = row["Name"].replace(" - Full Art", "") From bdb958b420c19e02d4d7b03db13394f40c0392e1 Mon Sep 17 00:00:00 2001 From: Robert Konell Date: Wed, 5 Jan 2022 01:22:31 -0800 Subject: [PATCH 11/17] linting commit + updates to README specifying the use of black as the python linter --- .gitignore | 1 + README.md | 13 ++++++ src/tcg-to-deckbox.py | 105 +++++++++++++++++++++++++++--------------- 3 files changed, 83 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 6ecb909..1071c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build dist multiple_names.json __pycache__ +.vscode/settings.json diff --git a/README.md b/README.md index 751c849..f8f7bde 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,16 @@ In both cases, it will give you a new csv, named `deckbox_import.csv` in the dir 2. Showcase and extended art cards - TCGplayer suffixes these with `(Extended Art)` or `(Showcase)`. If the script finds these terms, it just deletes them. They're unnecessary as they have different collector's numbers. 3. Odd sets from Magic's history - I did my best here, but some of the older sets didn't quite line up. Some I just didn't have enough information on, some looked like categories of multiple sets, and some actually look like they map to multple different sets in deckbox. If you hit some of these, manual correction is probably the best bet for now, but if you have a solution, feel free to share! + +## Contributing +When making pull requests please format Python code using Black [black](https://github.com/psf/black) + +```sh +# installing black +pip install black +``` + +```sh +# running black +black tcg-to-deckbox.py +``` diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 1d137bc..c77b2ad 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -16,17 +16,31 @@ # Constants MULTI_NAMES_FILE = "multiple_names.json" SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" -MULTI_NAMES_IGNORE = ["Nicol Bolas, the Ravager","Hadana's Climb", "Treasure Map", "Storm the Vault"] - -#global vars +MULTI_NAMES_IGNORE = [ + "Nicol Bolas, the Ravager", + "Hadana's Climb", + "Treasure Map", + "Storm the Vault", +] + +# global vars scryfall_data = {} -#Global replacement helpers -ixalan_bab = ["Legion's Landing", "Search for Azcanta", "Arguel's Blood Fast", "Vance's Blasting Cannons", "Growing Rites of Itlimoc", "Conqueror's Galleon", "Dowsing Dagger", "Primal Amulet", "Thaumatic Compass", "Treasure Map"] - -bab_mapping = { - "Impervious Greatwurm": "Guilds of Ravnica" -} +# Global replacement helpers +ixalan_bab = [ + "Legion's Landing", + "Search for Azcanta", + "Arguel's Blood Fast", + "Vance's Blasting Cannons", + "Growing Rites of Itlimoc", + "Conqueror's Galleon", + "Dowsing Dagger", + "Primal Amulet", + "Thaumatic Compass", + "Treasure Map", +] + +bab_mapping = {"Impervious Greatwurm": "Guilds of Ravnica"} # Get rid of the root TK window, we don't need it. root = tk.Tk() @@ -34,7 +48,7 @@ def fetch_multiple_names(uri, page=1): - print('Begin: Download %s, page %s of results' % (uri, page)) + print("Begin: Download %s, page %s of results" % (uri, page)) try: with requests.get(uri) as response: tmp_scryfall_data = response.json() @@ -48,9 +62,10 @@ def fetch_multiple_names(uri, page=1): if "next_page" in tmp_scryfall_data: fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) except Exception: - print('Exception: Was unable to download %s, page %s of results' % (uri, page)) + print("Exception: Was unable to download %s, page %s of results" % (uri, page)) print(Exception) + # Utility function to replace strings in the csv from the replacements.config file. @@ -72,14 +87,16 @@ def getPathPrefix(): # Check to see if we have DFC/Split/etc card names from scryfall and if it is up to date try: multi_files_last_updated = os.path.getmtime(MULTI_NAMES_FILE) - print("%s last modified: %s" % - (MULTI_NAMES_FILE, time.ctime(multi_files_last_updated))) + print( + "%s last modified: %s" + % (MULTI_NAMES_FILE, time.ctime(multi_files_last_updated)) + ) now = time.time() - last_week = now - 60*60*24*7 + last_week = now - 60 * 60 * 24 * 7 if multi_files_last_updated < last_week: print("File %s is stale - updating..." % (MULTI_NAMES_FILE)) fetch_multiple_names(SCRYFALL_URL) - with open(MULTI_NAMES_FILE, 'w') as multiple_names: + with open(MULTI_NAMES_FILE, "w") as multiple_names: json.dump(scryfall_data, multiple_names) print("Done!") else: @@ -91,7 +108,7 @@ def getPathPrefix(): except Exception: print("File %s not found - creating..." % (MULTI_NAMES_FILE)) fetch_multiple_names(SCRYFALL_URL) - with open(MULTI_NAMES_FILE, 'w') as multiple_names: + with open(MULTI_NAMES_FILE, "w") as multiple_names: json.dump(scryfall_data, multiple_names) print("Done!") @@ -100,23 +117,36 @@ def getPathPrefix(): GUI = False if len(sys.argv) < 2: GUI = True - FILE = filedialog.askopenfilename(title="Select your TCGPlayer app export file", filetypes=[ - ("TCGPlayer exports", ".csv"), ("All files", "*.*")]) + FILE = filedialog.askopenfilename( + title="Select your TCGPlayer app export file", + filetypes=[("TCGPlayer exports", ".csv"), ("All files", "*.*")], + ) if len(FILE) == 0: - messagebox.showerror(title="Input file not provided", - message="You must pass the TCGPlayer csv export file to this program.") + messagebox.showerror( + title="Input file not provided", + message="You must pass the TCGPlayer csv export file to this program.", + ) sys.exit() else: FILE = sys.argv[1] -skipcolumns = ["Simple Name", "Set Code", "Rarity", - "Product ID", "SKU", "Price", "Price Each"] +skipcolumns = [ + "Simple Name", + "Set Code", + "Rarity", + "Product ID", + "SKU", + "Price", + "Price Each", +] outputFile = "deckbox_import.csv" configParser = configparser.ConfigParser(delimiters="=") configParser.read(os.path.join(getPathPrefix(), "replacements.config")) -with open(FILE, newline="") as tcgcsvfile, open(outputFile, "w", newline="") as deckboxcsvfile: +with open(FILE, newline="") as tcgcsvfile, open( + outputFile, "w", newline="" +) as deckboxcsvfile: try: csv.Sniffer().sniff(tcgcsvfile.read(4096), delimiters=",") @@ -124,7 +154,9 @@ def getPathPrefix(): except: if GUI: messagebox.showerror( - title="Invalid input file", message="The file selected does not appear to be a valid CSV file.") + title="Invalid input file", + message="The file selected does not appear to be a valid CSV file.", + ) else: print("The file passed does not appear to be a valid CSV file.") sys.exit() @@ -141,14 +173,15 @@ def getPathPrefix(): headersdeckbox = [x for x in headerstcg if x not in skipcolumns] csvwriter = csv.DictWriter( - deckboxcsvfile, quoting=csv.QUOTE_ALL, fieldnames=headersdeckbox) + deckboxcsvfile, quoting=csv.QUOTE_ALL, fieldnames=headersdeckbox + ) csvwriter.writeheader() for row in csvreader: - skip_scryfall_names=False - + skip_scryfall_names = False + # Don't bother with columns that are going to be ignored anyways for skippable in skipcolumns: - row.pop(skippable, '') + row.pop(skippable, "") # Map the printing column to the Foil column if row["Foil"] == "Normal": @@ -180,17 +213,17 @@ def getPathPrefix(): # Buy a Box Promos worled a little differently with Ixalan if row["Name"] in ixalan_bab and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = "Black Friday Treasure Chest Promos" - skip_scryfall_names=True + skip_scryfall_names = True # Handle other BaB promo cards if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = bab_mapping[row["Name"]] # Handle Mystery Booster Test Cards, the 2021 release differentiates by Edition # on deckbox, while tcgplayer differentiates by name appending '(No PW Symbol)' - if "(No PW Symbol)" in row["Name"] and row["Edition"] == "Mystery Booster: Convention Edition Exclusives": + if ( + "(No PW Symbol)" in row["Name"] + and row["Edition"] == "Mystery Booster: Convention Edition Exclusives" + ): row["Edition"] = "Mystery Booster Playtest Cards 2021" - - # For BFZ lands...there's no differentiator from the full arts and the non full arts. - row["Name"] = row["Name"].replace(" - Full Art", "") row["Name"] = re.sub(r" \(.*\)", "", row["Name"]) replace_strings(row, "NAMES", "Name") @@ -208,9 +241,9 @@ def getPathPrefix(): # All Done! successMsg = "Your import file for deckbox.org is available here: %s" % os.path.abspath( - outputFile) + outputFile +) if GUI: - messagebox.showinfo( - title="Conversion completed successfully!", message=successMsg) + messagebox.showinfo(title="Conversion completed successfully!", message=successMsg) else: print(successMsg) From 9bbff9a27164273aa22465ca34f2658957c7bf45 Mon Sep 17 00:00:00 2001 From: Robert Konell Date: Wed, 5 Jan 2022 01:55:52 -0800 Subject: [PATCH 12/17] added more card rules, moved alternative art rules to replacements.config as i should have done originally --- src/replacements.config | 9 +++++++++ src/tcg-to-deckbox.py | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/replacements.config b/src/replacements.config index 8ca697e..95e199d 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -82,3 +82,12 @@ Tamiyo's Journal (Entry 546)=Tamiyo's Journal Tamiyo's Journal (Entry 855)=Tamiyo's Journal Tamiyo's Journal (Entry 434)=Tamiyo's Journal Tamiyo's Journal (Entry 653)=Tamiyo's Journal +;; Crimson Vow Alternative Art +Sisters of the Undead - Olivia, Crimson Bride=Olivia, Crimson Bride +Mina Harker - Thalia, Guardian of Thraben=Thalia, Guardian of Thraben +Abraham Van Helsing - Savior of Ollenbock=Savior of Ollenbock +Dracula, Lord of Blood - Voldaren Bloodcaster=Voldaren Bloodcaster +Dracula the Voyager - Edgar, Charmed Groom=Edgar, Charmed Groom +Dracula, Blood Immortal - Falkenrath Forebear=Falkenrath Forebear +;; Ikoria Alternative Art +Destoroyah, Perfect Lifeform - Everquill Phoenix=Everquill Phoenix diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index c77b2ad..f8ce35d 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -21,6 +21,9 @@ "Hadana's Climb", "Treasure Map", "Storm the Vault", + "Hanweir Militia Captain", + "Legion's Landing", + "Geier Reach Bandit", ] # global vars @@ -40,7 +43,12 @@ "Treasure Map", ] -bab_mapping = {"Impervious Greatwurm": "Guilds of Ravnica"} +bab_mapping = { + "Impervious Greatwurm": "Guilds of Ravnica", + "Kenrith, the Returned King": "Throne of Eldraine", + "Realmwalker": "Kaldheim", + "Vorpal Sword": "Adventures in the Forgotten Realms", +} # Get rid of the root TK window, we don't need it. root = tk.Tk() @@ -194,14 +202,6 @@ def getPathPrefix(): replace_strings(row, "LANGUAGES", "Language") # Map Specific Card Names, and drop extra tidbits - # alternative art/name for Dracula cards - row["Name"] = row["Name"].replace("Sisters of the Undead - ", "") - row["Name"] = row["Name"].replace("Mina Harker - ", "") - row["Name"] = row["Name"].replace("Abraham Van Helsing - ", "") - row["Name"] = row["Name"].replace("Dracula, Lord of Blood - ", "") - row["Name"] = row["Name"].replace("Dracula the Voyager - ", "") - row["Name"] = row["Name"].replace("Dracula, Blood Immortal - ", "") - # For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"] = row["Name"].replace(" - Full Art", "") @@ -214,9 +214,6 @@ def getPathPrefix(): if row["Name"] in ixalan_bab and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = "Black Friday Treasure Chest Promos" skip_scryfall_names = True - # Handle other BaB promo cards - if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": - row["Edition"] = bab_mapping[row["Name"]] # Handle Mystery Booster Test Cards, the 2021 release differentiates by Edition # on deckbox, while tcgplayer differentiates by name appending '(No PW Symbol)' if ( @@ -229,6 +226,9 @@ def getPathPrefix(): replace_strings(row, "NAMES", "Name") if skip_scryfall_names == False and row["Name"] in scryfall_data: row["Name"] = scryfall_data[row["Name"]] + # Fix edition for Buy-A-Box Promos + if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": + row["Edition"] = bab_mapping[row["Name"]] # remove weird symbols from card numbers row["Card Number"] = re.sub(r"[*★]", "", row["Card Number"]) From 790a7a8a366380ea509479c58f07505c7f4fb189 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:25:39 -0500 Subject: [PATCH 13/17] Checking deckbox to see if a card name exists in the case of double faced / split cards --- README.md | 10 +++--- src/tcg-to-deckbox.py | 84 ++++++++++++++++++++++++++----------------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index f8f7bde..404adac 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ https://deckbox.org/ is a popular tool for cataloging one's [Magic: the Gatherin The script has two modes: CLI and GUI. To use the CLI, provide the path to your TCGplayer csv file: python3 tcg-to-deckbox.py ~/bin/sample_input.csv - + To start it in GUI mode, just launch the program without any arguments python3 tcg-to-deckbox.py - -In both cases, it will give you a new csv, named `deckbox_import.csv` in the directory from which the script was run. + +In both cases, it will give you a new csv, named `deckbox_import.csv` in the directory from which the script was run. ## Limitations @@ -23,10 +23,10 @@ In both cases, it will give you a new csv, named `deckbox_import.csv` in the dir 2. Showcase and extended art cards - TCGplayer suffixes these with `(Extended Art)` or `(Showcase)`. If the script finds these terms, it just deletes them. They're unnecessary as they have different collector's numbers. -3. Odd sets from Magic's history - I did my best here, but some of the older sets didn't quite line up. Some I just didn't have enough information on, some looked like categories of multiple sets, and some actually look like they map to multple different sets in deckbox. If you hit some of these, manual correction is probably the best bet for now, but if you have a solution, feel free to share! +3. Odd sets from Magic's history - I did my best here, but some of the older sets didn't quite line up. Some I just didn't have enough information on, some looked like categories of multiple sets, and some actually look like they map to multiple different sets in deckbox. If you hit some of these, manual correction is probably the best bet for now, but if you have a solution, feel free to share! ## Contributing -When making pull requests please format Python code using Black [black](https://github.com/psf/black) +When making pull requests please format Python code using Black [black](https://github.com/psf/black) ```sh # installing black diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index f8ce35d..586a0ce 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -9,24 +9,17 @@ from tkinter import messagebox import tkinter as tk import requests +import urllib.parse import ssl import json # Constants MULTI_NAMES_FILE = "multiple_names.json" -SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" -MULTI_NAMES_IGNORE = [ - "Nicol Bolas, the Ravager", - "Hadana's Climb", - "Treasure Map", - "Storm the Vault", - "Hanweir Militia Captain", - "Legion's Landing", - "Geier Reach Bandit", -] +SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" +DECKBOX_URL = "https://deckbox.org/mtg/" -# global vars +# Global Data scryfall_data = {} # Global replacement helpers @@ -54,19 +47,17 @@ root = tk.Tk() root.withdraw() - +# Queries scryfall to build a list of cards that have multiple names. def fetch_multiple_names(uri, page=1): - print("Begin: Download %s, page %s of results" % (uri, page)) + print( + "Begin: Download '%s', page %s of results" % (urllib.parse.unquote(uri), page) + ) try: - with requests.get(uri) as response: - tmp_scryfall_data = response.json() + with requests.get(uri) as scryfall_response: + tmp_scryfall_data = scryfall_response.json() for x in tmp_scryfall_data["data"]: - if x["card_faces"][0]["name"] in MULTI_NAMES_IGNORE: - # detected a card we want to ignore from scryfall - continue - else: - scryfall_data[x["card_faces"][0]["name"]] = x["name"] + scryfall_data[x["card_faces"][0]["name"]] = x["name"] if "next_page" in tmp_scryfall_data: fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) except Exception: @@ -75,13 +66,12 @@ def fetch_multiple_names(uri, page=1): # Utility function to replace strings in the csv from the replacements.config file. - - def replace_strings(dict, replacementSection, columnName): if dict[columnName].lower() in configParser[replacementSection].keys(): dict[columnName] = configParser[replacementSection][dict[columnName].lower()] +# Utility function to handle differences in lookup path if there's a UI involved. def getPathPrefix(): try: # PyInstaller creates a temp folder and stores path in _MEIPASS @@ -94,14 +84,16 @@ def getPathPrefix(): # Check to see if we have DFC/Split/etc card names from scryfall and if it is up to date try: - multi_files_last_updated = os.path.getmtime(MULTI_NAMES_FILE) + multi_names_file_last_updated = os.path.getmtime(MULTI_NAMES_FILE) print( "%s last modified: %s" - % (MULTI_NAMES_FILE, time.ctime(multi_files_last_updated)) + % (MULTI_NAMES_FILE, time.ctime(multi_names_file_last_updated)) ) now = time.time() last_week = now - 60 * 60 * 24 * 7 - if multi_files_last_updated < last_week: + + # Refresh the multiple names file if it's a week old, else use the cached version + if multi_names_file_last_updated < last_week: print("File %s is stale - updating..." % (MULTI_NAMES_FILE)) fetch_multiple_names(SCRYFALL_URL) with open(MULTI_NAMES_FILE, "w") as multiple_names: @@ -112,7 +104,7 @@ def getPathPrefix(): with open(MULTI_NAMES_FILE) as multiple_names: scryfall_data = json.load(multiple_names) print("Done!") - +# If the file isn't found, create it except Exception: print("File %s not found - creating..." % (MULTI_NAMES_FILE)) fetch_multiple_names(SCRYFALL_URL) @@ -201,19 +193,23 @@ def getPathPrefix(): # Map Chinese Languages replace_strings(row, "LANGUAGES", "Language") - # Map Specific Card Names, and drop extra tidbits + #################################################################### + # Map Specific Card Conditions + #################################################################### + # For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"] = row["Name"].replace(" - Full Art", "") - # Very specifc conditons - # war of the spark Alternate arts handled differently + # War of the Spark Alternate Arts handled differently if "(JP Alternate Art)" in row["Name"] and row["Edition"] == "War of the Spark": row["Edition"] = "War of the Spark Japanese Alternate Art" row["Name"] = row["Name"].replace(" (JP Alternate Art)", "") - # Buy a Box Promos worled a little differently with Ixalan + + # Buy a Box Promos worked a little differently with Ixalan if row["Name"] in ixalan_bab and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = "Black Friday Treasure Chest Promos" skip_scryfall_names = True + # Handle Mystery Booster Test Cards, the 2021 release differentiates by Edition # on deckbox, while tcgplayer differentiates by name appending '(No PW Symbol)' if ( @@ -222,10 +218,34 @@ def getPathPrefix(): ): row["Edition"] = "Mystery Booster Playtest Cards 2021" + #################################################################### + # Handle General Card Conversions + #################################################################### + + # Remove all Parentheses at the end of cards row["Name"] = re.sub(r" \(.*\)", "", row["Name"]) replace_strings(row, "NAMES", "Name") - if skip_scryfall_names == False and row["Name"] in scryfall_data: - row["Name"] = scryfall_data[row["Name"]] + # We need to do a little extra work on dual faced cards, because deckbox is inconsistent with whether it refers to cards by both names or just the front face. + if row["Name"] in scryfall_data: + deckbox_request_url = DECKBOX_URL + urllib.parse.quote( + scryfall_data[row["Name"]] + ) + with requests.get(deckbox_request_url) as deckbox_response: + + # If we are not redirected to a new page, then we should only use the front face name + if deckbox_request_url == deckbox_response.url: + print( + "Dual name for '%s' found on deckbox, the dual name will be used for the import." + % (scryfall_data[row["Name"]], scryfall_data[row["Name"]]) + ) + else: + print( + "Dual name not found for '%s' on deckbox, front face name '%s' will be used." + % (scryfall_data[row["Name"]], row["Name"]) + ) + skip_scryfall_names = True + if skip_scryfall_names == False: + row["Name"] = scryfall_data[row["Name"]] # Fix edition for Buy-A-Box Promos if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = bab_mapping[row["Name"]] From b0b83069361b4ad321c8b22f815b5663d80aa569 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Sat, 18 Jun 2022 22:39:20 -0400 Subject: [PATCH 14/17] some minor fixes and more replacements --- requirements.txt | 2 +- src/replacements.config | 20 +++++++++++++------- src/tcg-to-deckbox.py | 7 +++++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8b13789..14f8bb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ - +certifi==2021.10.8 diff --git a/src/replacements.config b/src/replacements.config index 95e199d..254dbc3 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -74,14 +74,10 @@ Time Spiral: Remastered=Time Spiral Remastered Commander: Adventures in the Forgotten Realms=Adventures in the Forgotten Realms Commander Commander: Innistrad: Midnight Hunt=Innistrad: Midnight Hunt Commander Commander: Innistrad: Crimson Vow=Innistrad: Crimson Vow Commander - +SLX Cards=Universes Within +Promo Pack: Streets of New Capenna=Streets of New Capenna Promo Pack +Commander: Streets of New Capenna=Streets of New Capenna Commander [NAMES] -Tamiyo's Journal (Entry 922)=Tamiyo's Journal -Tamiyo's Journal (Entry 711)=Tamiyo's Journal -Tamiyo's Journal (Entry 546)=Tamiyo's Journal -Tamiyo's Journal (Entry 855)=Tamiyo's Journal -Tamiyo's Journal (Entry 434)=Tamiyo's Journal -Tamiyo's Journal (Entry 653)=Tamiyo's Journal ;; Crimson Vow Alternative Art Sisters of the Undead - Olivia, Crimson Bride=Olivia, Crimson Bride Mina Harker - Thalia, Guardian of Thraben=Thalia, Guardian of Thraben @@ -91,3 +87,13 @@ Dracula the Voyager - Edgar, Charmed Groom=Edgar, Charmed Groom Dracula, Blood Immortal - Falkenrath Forebear=Falkenrath Forebear ;; Ikoria Alternative Art Destoroyah, Perfect Lifeform - Everquill Phoenix=Everquill Phoenix + +[SPECIAL_NAMES] +;; A rare few cards actually have Parentheses as part of the name. We don't want to strip these. +B.F.M. (Big Furry Monster) (Left)=B.F.M. (Big Furry Monster Left) +B.F.M. (Big Furry Monster) (Right)=B.F.M. (Big Furry Monster Right) +B.O.B. (Bevy of Beebles)=B.O.B. (Bevy of Beebles) +Erase (Not the Urza's Legacy One)=Erase (Not the Urza's Legacy One) +Hazmat Suit (Used)=Hazmat Suit (Used) + +[ALTERNATE_ARTS] diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 586a0ce..b2ecc53 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -233,10 +233,13 @@ def getPathPrefix(): with requests.get(deckbox_request_url) as deckbox_response: # If we are not redirected to a new page, then we should only use the front face name - if deckbox_request_url == deckbox_response.url: + deckbox_response_url_parts = urllib.parse.urlparse(deckbox_response.url) + deckbox_response_url = deckbox_response_url_parts._replace(fragment="")._replace(query="").geturl() + + if deckbox_request_url == deckbox_response_url or deckbox_request_url.replace("//", "/") == deckbox_response_url.replace("//", "/"): print( "Dual name for '%s' found on deckbox, the dual name will be used for the import." - % (scryfall_data[row["Name"]], scryfall_data[row["Name"]]) + % (scryfall_data[row["Name"]]) ) else: print( From 34e4ff2164ab83b18e5c00900cefe23fae7a0778 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Sun, 13 Aug 2023 01:51:01 -0400 Subject: [PATCH 15/17] Making promo packs, buy a box, and commander editions smoother --- src/replacements.config | 5 ++- src/tcg-to-deckbox.py | 88 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/src/replacements.config b/src/replacements.config index 254dbc3..4202958 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -75,8 +75,11 @@ Commander: Adventures in the Forgotten Realms=Adventures in the Forgotten Realms Commander: Innistrad: Midnight Hunt=Innistrad: Midnight Hunt Commander Commander: Innistrad: Crimson Vow=Innistrad: Crimson Vow Commander SLX Cards=Universes Within -Promo Pack: Streets of New Capenna=Streets of New Capenna Promo Pack Commander: Streets of New Capenna=Streets of New Capenna Commander +The Brothers' War: Retro Frame Artifacts=The Brothers' War Retro Artifacts +March of the Machine: Multiverse Legends=Multiverse Legends +Promo Pack: Kamigawa: Neon Dynasty=Promo Pack: Kamigawa: Neon Dynasty + [NAMES] ;; Crimson Vow Alternative Art Sisters of the Undead - Olivia, Crimson Bride=Olivia, Crimson Bride diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index b2ecc53..9d00158 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -12,15 +12,19 @@ import urllib.parse import ssl import json - +import traceback # Constants MULTI_NAMES_FILE = "multiple_names.json" -SCRYFALL_URL = "https://api.scryfall.com/cards/search?order=cmc&q=%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" +BAB_FILE = "bab.json" +SCRYFALL_BASE_URL = "https://api.scryfall.com/cards/search?order=cmc&q=" +SCRYFALL_DFC_URL = SCRYFALL_BASE_URL + "%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" +SCRYFALL_BAB_URL = SCRYFALL_BASE_URL + "is%3Abab+AND+game%3Apaper" DECKBOX_URL = "https://deckbox.org/mtg/" # Global Data scryfall_data = {} +scryfall_bab_data = {} # Global replacement helpers ixalan_bab = [ @@ -62,9 +66,25 @@ def fetch_multiple_names(uri, page=1): fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) except Exception: print("Exception: Was unable to download %s, page %s of results" % (uri, page)) - print(Exception) - + print(str(Exception)) + +# Queries scryfall to build a list of cards that were buy a box promos. +def fetch_bab_names(uri, page=1): + print( + "Begin: Download '%s', page %s of results" % (urllib.parse.unquote(uri), page) + ) + try: + with requests.get(uri) as scryfall_response: + tmp_scryfall_data = scryfall_response.json() + for x in tmp_scryfall_data["data"]: + scryfall_bab_data[x["name"]] = x["set_name"] + if "next_page" in tmp_scryfall_data: + fetch_bab_names(tmp_scryfall_data["next_page"], page + 1) + except Exception: + print("Exception: Was unable to download %s, page %s of results" % (uri, page)) + traceback.print_exc() + # Utility function to replace strings in the csv from the replacements.config file. def replace_strings(dict, replacementSection, columnName): if dict[columnName].lower() in configParser[replacementSection].keys(): @@ -95,7 +115,7 @@ def getPathPrefix(): # Refresh the multiple names file if it's a week old, else use the cached version if multi_names_file_last_updated < last_week: print("File %s is stale - updating..." % (MULTI_NAMES_FILE)) - fetch_multiple_names(SCRYFALL_URL) + fetch_multiple_names(SCRYFALL_DFC_URL) with open(MULTI_NAMES_FILE, "w") as multiple_names: json.dump(scryfall_data, multiple_names) print("Done!") @@ -107,12 +127,43 @@ def getPathPrefix(): # If the file isn't found, create it except Exception: print("File %s not found - creating..." % (MULTI_NAMES_FILE)) - fetch_multiple_names(SCRYFALL_URL) + fetch_multiple_names(SCRYFALL_DFC_URL) with open(MULTI_NAMES_FILE, "w") as multiple_names: json.dump(scryfall_data, multiple_names) print("Done!") +# Check to see if we have Buy a Box card names from scryfall and if it is up to date +try: + bab_file_last_updated = os.path.getmtime(BAB_FILE) + print( + "%s last modified: %s" + % (BAB_FILE, time.ctime(bab_file_last_updated)) + ) + now = time.time() + last_week = now - 60 * 60 * 24 * 7 + + # Refresh the multiple names file if it's a week old, else use the cached version + if bab_file_last_updated < last_week: + print("File %s is stale - updating..." % (BAB_FILE)) + fetch_bab_names(SCRYFALL_BAB_URL) + with open(BAB_FILE, "w") as multiple_names: + json.dump(scryfall_bab_data, multiple_names) + print("Done!") + else: + print("Using existing %s file..." % BAB_FILE) + with open(BAB_FILE) as multiple_names: + scryfall_bab_data = json.load(multiple_names) + print("Done!") +# If the file isn't found, create it +except Exception: + print("File %s not found - creating..." % (BAB_FILE)) + fetch_bab_names(SCRYFALL_BAB_URL) + with open(BAB_FILE, "w") as multiple_names: + json.dump(scryfall_bab_data, multiple_names) + print("Done!") + + # Get our input GUI = False if len(sys.argv) < 2: @@ -199,7 +250,7 @@ def getPathPrefix(): # For BFZ lands...there's no differentiator from the full arts and the non full arts. row["Name"] = row["Name"].replace(" - Full Art", "") - + # War of the Spark Alternate Arts handled differently if "(JP Alternate Art)" in row["Name"] and row["Edition"] == "War of the Spark": row["Edition"] = "War of the Spark Japanese Alternate Art" @@ -209,6 +260,13 @@ def getPathPrefix(): if row["Name"] in ixalan_bab and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = "Black Friday Treasure Chest Promos" skip_scryfall_names = True + + # TODO Merge this with above + if row["Name"] in scryfall_bab_data and row["Edition"] == "Buy-A-Box Promos": + if "Promos" in scryfall_bab_data[row["Name"]]: + row["Edition"] = "Media Inserts" + else: + row["Edition"] = scryfall_bab_data[row["Name"]] # Handle Mystery Booster Test Cards, the 2021 release differentiates by Edition # on deckbox, while tcgplayer differentiates by name appending '(No PW Symbol)' @@ -225,6 +283,7 @@ def getPathPrefix(): # Remove all Parentheses at the end of cards row["Name"] = re.sub(r" \(.*\)", "", row["Name"]) replace_strings(row, "NAMES", "Name") + # We need to do a little extra work on dual faced cards, because deckbox is inconsistent with whether it refers to cards by both names or just the front face. if row["Name"] in scryfall_data: deckbox_request_url = DECKBOX_URL + urllib.parse.quote( @@ -249,15 +308,28 @@ def getPathPrefix(): skip_scryfall_names = True if skip_scryfall_names == False: row["Name"] = scryfall_data[row["Name"]] + # Fix edition for Buy-A-Box Promos if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": row["Edition"] = bab_mapping[row["Name"]] + + # Move commander from edition to the end + if "Commander: " in row["Edition"]: + row["Edition"] = re.sub(r"Commander: (.*)$", r"\1 Commander", row["Edition"]) + + # Remove Universes Beyond modifier + if "Universes Beyond: " in row["Edition"]: + row["Edition"] = re.sub(r"Universes Beyond: ", "", row["Edition"]) # remove weird symbols from card numbers row["Card Number"] = re.sub(r"[*★]", "", row["Card Number"]) + + # move Promo Pack to the end if it's not in replacements.config (some old ones actually do have the prefix + if "Promo Pack: " in row["Edition"] and not row["Edition"] in configParser["EDITONS"].keys(): + row["Edition"] = re.sub(r"Promo Pack: (.*)$", r"\1 Promo Pack", row["Edition"]) # Map Specific Edition Names - replace_strings(row, "EDITONS", "Edition") + replace_strings(row, "EDITONS", "Edition") # write the converted output csvwriter.writerow(row) From 24064721b52d61aff4498b5976ed62520cf1563a Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Tue, 5 Mar 2024 23:58:58 -0500 Subject: [PATCH 16/17] Code cleanup --- .idea/.gitignore | 3 + requirements.txt | 1 + src/replacements.config | 4 +- src/tcg-to-deckbox.py | 520 +++++++++++++++++----------------------- 4 files changed, 233 insertions(+), 295 deletions(-) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/requirements.txt b/requirements.txt index 14f8bb0..3331531 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ certifi==2021.10.8 +requests~=2.31.0 \ No newline at end of file diff --git a/src/replacements.config b/src/replacements.config index 4202958..05e10e3 100644 --- a/src/replacements.config +++ b/src/replacements.config @@ -12,7 +12,8 @@ Damaged=Poor Chinese (S)=Chinese Chinese (T)=Chinese Traditional -[EDITONS] +[EDITIONS] +The List Reprints=The List Masterpiece Series: Amonkhet Invocations=Amonkhet Invocations Arena Promos=Arena League APAC Lands=Asia Pacific Land Program @@ -79,6 +80,7 @@ Commander: Streets of New Capenna=Streets of New Capenna Commander The Brothers' War: Retro Frame Artifacts=The Brothers' War Retro Artifacts March of the Machine: Multiverse Legends=Multiverse Legends Promo Pack: Kamigawa: Neon Dynasty=Promo Pack: Kamigawa: Neon Dynasty +XLN Treasure Chest=Black Friday Treasure Chest Promos [NAMES] ;; Crimson Vow Alternative Art diff --git a/src/tcg-to-deckbox.py b/src/tcg-to-deckbox.py index 9d00158..6a82cae 100644 --- a/src/tcg-to-deckbox.py +++ b/src/tcg-to-deckbox.py @@ -1,75 +1,77 @@ -import sys +import configparser import csv -import time +import json import os import os.path -import configparser import re -from tkinter import filedialog -from tkinter import messagebox -import tkinter as tk -import requests -import urllib.parse -import ssl -import json +import sys +import time import traceback +import urllib.parse +from csv import DictWriter, DictReader +from typing import TextIO + +import requests # Constants MULTI_NAMES_FILE = "multiple_names.json" BAB_FILE = "bab.json" SCRYFALL_BASE_URL = "https://api.scryfall.com/cards/search?order=cmc&q=" -SCRYFALL_DFC_URL = SCRYFALL_BASE_URL + "%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20AND%20game%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra" +# noinspection SpellCheckingInspection +SCRYFALL_DFC_URL = SCRYFALL_BASE_URL + "%28is%3Adoublesided%20OR%20is%3Asplit%20OR%20is%3Aadventure%29%20AND%20game" \ + "%3Apaper%20AND%20-is%3Atoken%20AND%20-set%3ACMB1%20AND%20-is%3Aextra " +# noinspection SpellCheckingInspection SCRYFALL_BAB_URL = SCRYFALL_BASE_URL + "is%3Abab+AND+game%3Apaper" DECKBOX_URL = "https://deckbox.org/mtg/" +SKIP_COLUMNS = [ + "Simple Name", + "Set Code", + "Rarity", + "Product ID", + "SKU", + "Price", + "Price Each", +] +OUTPUT_FILE = "deckbox_import.csv" + # Global Data scryfall_data = {} scryfall_bab_data = {} -# Global replacement helpers -ixalan_bab = [ - "Legion's Landing", - "Search for Azcanta", - "Arguel's Blood Fast", - "Vance's Blasting Cannons", - "Growing Rites of Itlimoc", - "Conqueror's Galleon", - "Dowsing Dagger", - "Primal Amulet", - "Thaumatic Compass", - "Treasure Map", -] +# So as not to hammer scryfall with unnecessary requests, we check to see if our cached data is older than a week. +now = time.time() +last_week = now - 60 * 60 * 24 * 7 -bab_mapping = { - "Impervious Greatwurm": "Guilds of Ravnica", - "Kenrith, the Returned King": "Throne of Eldraine", - "Realmwalker": "Kaldheim", - "Vorpal Sword": "Adventures in the Forgotten Realms", -} -# Get rid of the root TK window, we don't need it. -root = tk.Tk() -root.withdraw() +def get_path_prefix() -> str: + """ + Returns the path where all files except the input csv are located. This includes the replacements.config, as well as + the output deckbox_import.csv. This is a holdover from when this was packaged as an executable. In that, it needed + a different variable to be told to look inside the bundled program. Now it will always return the current directory. + :rtype: str + :return: the current directory the script is run from. + """ + prefix: str = os.path.abspath(".") + return prefix -# Queries scryfall to build a list of cards that have multiple names. -def fetch_multiple_names(uri, page=1): - print( - "Begin: Download '%s', page %s of results" % (urllib.parse.unquote(uri), page) - ) - try: - with requests.get(uri) as scryfall_response: - tmp_scryfall_data = scryfall_response.json() - for x in tmp_scryfall_data["data"]: - scryfall_data[x["card_faces"][0]["name"]] = x["name"] - if "next_page" in tmp_scryfall_data: - fetch_multiple_names(tmp_scryfall_data["next_page"], page + 1) - except Exception: - print("Exception: Was unable to download %s, page %s of results" % (uri, page)) - print(str(Exception)) - -# Queries scryfall to build a list of cards that were buy a box promos. -def fetch_bab_names(uri, page=1): +# Utility function to replace strings in the csv from the replacements.config file. +def replace_strings(dictionary, config_parser, replacement_section, column_name): + if dictionary[column_name].lower() in config_parser[replacement_section].keys(): + dictionary[column_name] = config_parser[replacement_section][dictionary[column_name].lower()] + + +def scryfall_data_func(raw_data: dict): + scryfall_data[raw_data["card_faces"][0]["name"]] = raw_data["name"] + + +def scryfall_bab_data_func(raw_data: dict): + scryfall_bab_data[raw_data["name"]] = raw_data["set_name"] + + +# Queries scryfall to build a list of cards that have multiple names. +def fetch_scryfall_data(uri, mapper, page=1): print( "Begin: Download '%s', page %s of results" % (urllib.parse.unquote(uri), page) ) @@ -78,267 +80,197 @@ def fetch_bab_names(uri, page=1): tmp_scryfall_data = scryfall_response.json() for x in tmp_scryfall_data["data"]: - scryfall_bab_data[x["name"]] = x["set_name"] + mapper(x) if "next_page" in tmp_scryfall_data: - fetch_bab_names(tmp_scryfall_data["next_page"], page + 1) - except Exception: + fetch_scryfall_data(tmp_scryfall_data["next_page"], mapper, page + 1) + except requests.exceptions.JSONDecodeError: print("Exception: Was unable to download %s, page %s of results" % (uri, page)) traceback.print_exc() - -# Utility function to replace strings in the csv from the replacements.config file. -def replace_strings(dict, replacementSection, columnName): - if dict[columnName].lower() in configParser[replacementSection].keys(): - dict[columnName] = configParser[replacementSection][dict[columnName].lower()] -# Utility function to handle differences in lookup path if there's a UI involved. -def getPathPrefix(): +def load_scryfall_data(filename: str, query_url: str, mapper_function, data_dict: dict): try: - # PyInstaller creates a temp folder and stores path in _MEIPASS - prefix = sys._MEIPASS - except Exception: - # else, use current directory - prefix = os.path.abspath(".") - return prefix - + file_last_updated: float = os.path.getmtime(filename) + print( + "%s last modified: %s" + % (filename, time.ctime(file_last_updated)) + ) -# Check to see if we have DFC/Split/etc card names from scryfall and if it is up to date -try: - multi_names_file_last_updated = os.path.getmtime(MULTI_NAMES_FILE) - print( - "%s last modified: %s" - % (MULTI_NAMES_FILE, time.ctime(multi_names_file_last_updated)) - ) - now = time.time() - last_week = now - 60 * 60 * 24 * 7 - - # Refresh the multiple names file if it's a week old, else use the cached version - if multi_names_file_last_updated < last_week: - print("File %s is stale - updating..." % (MULTI_NAMES_FILE)) - fetch_multiple_names(SCRYFALL_DFC_URL) - with open(MULTI_NAMES_FILE, "w") as multiple_names: - json.dump(scryfall_data, multiple_names) - print("Done!") - else: - print("Using existing %s file..." % MULTI_NAMES_FILE) - with open(MULTI_NAMES_FILE) as multiple_names: - scryfall_data = json.load(multiple_names) - print("Done!") -# If the file isn't found, create it -except Exception: - print("File %s not found - creating..." % (MULTI_NAMES_FILE)) - fetch_multiple_names(SCRYFALL_DFC_URL) - with open(MULTI_NAMES_FILE, "w") as multiple_names: - json.dump(scryfall_data, multiple_names) - print("Done!") - - -# Check to see if we have Buy a Box card names from scryfall and if it is up to date -try: - bab_file_last_updated = os.path.getmtime(BAB_FILE) - print( - "%s last modified: %s" - % (BAB_FILE, time.ctime(bab_file_last_updated)) - ) - now = time.time() - last_week = now - 60 * 60 * 24 * 7 - - # Refresh the multiple names file if it's a week old, else use the cached version - if bab_file_last_updated < last_week: - print("File %s is stale - updating..." % (BAB_FILE)) - fetch_bab_names(SCRYFALL_BAB_URL) - with open(BAB_FILE, "w") as multiple_names: - json.dump(scryfall_bab_data, multiple_names) - print("Done!") - else: - print("Using existing %s file..." % BAB_FILE) - with open(BAB_FILE) as multiple_names: - scryfall_bab_data = json.load(multiple_names) + # Refresh the multiple names file if it's a week old, else use the cached version + if file_last_updated < last_week: + print("File %s is stale - updating..." % filename) + fetch_scryfall_data(query_url, mapper_function) + with open(filename, "w") as file: + json.dump(data_dict, file) + print("Done!") + else: + print("Using existing %s file..." % filename) + with open(filename) as file: + data_dict.update(json.load(file)) + print("Done!") + # If the file isn't found, create it + except OSError: + print("File %s not found - creating..." % filename) + fetch_scryfall_data(query_url, mapper_function) + with open(filename, "w") as file: + json.dump(data_dict, file) print("Done!") -# If the file isn't found, create it -except Exception: - print("File %s not found - creating..." % (BAB_FILE)) - fetch_bab_names(SCRYFALL_BAB_URL) - with open(BAB_FILE, "w") as multiple_names: - json.dump(scryfall_bab_data, multiple_names) - print("Done!") - - -# Get our input -GUI = False -if len(sys.argv) < 2: - GUI = True - FILE = filedialog.askopenfilename( - title="Select your TCGPlayer app export file", - filetypes=[("TCGPlayer exports", ".csv"), ("All files", "*.*")], - ) - if len(FILE) == 0: - messagebox.showerror( - title="Input file not provided", - message="You must pass the TCGPlayer csv export file to this program.", + + +def process_row(row, writer, config_parser): + skip_scryfall_names = False + + # Don't bother with columns that are going to be ignored + skipped_column: str + for skipped_column in SKIP_COLUMNS: + row.pop(skipped_column, "") + + # Map the printing column to the Foil column + if row["Foil"] == "Normal": + row["Foil"] = "" + + # Map Card Condition + replace_strings(row, config_parser, "CONDITIONS", "Condition") + + # Map Chinese Languages + replace_strings(row, config_parser, "LANGUAGES", "Language") + + #################################################################### + # Map Specific Card Conditions + #################################################################### + + # For BFZ lands...there's no differentiator from the full arts and the non-full arts. + row["Name"] = row["Name"].replace(" - Full Art", "") + + # War of the Spark Alternate Arts handled differently + if "(JP Alternate Art)" in row["Name"] and row["Edition"] == "War of the Spark": + row["Edition"] = "War of the Spark Japanese Alternate Art" + row["Name"] = row["Name"].replace(" (JP Alternate Art)", "") + + if row["Name"] in scryfall_bab_data and row["Edition"] == "Buy-A-Box Promos": + if "Promos" in scryfall_bab_data[row["Name"]]: + row["Edition"] = "Media Inserts" + else: + row["Edition"] = scryfall_bab_data[row["Name"]] + + # Handle Mystery Booster Test Cards, the 2021 release differentiates by Edition + # on deckbox, while tcg player differentiates by name appending '(No PW Symbol)' + if ( + "(No PW Symbol)" in row["Name"] + and row["Edition"] == "Mystery Booster: Convention Edition Exclusives" + ): + row["Edition"] = "Mystery Booster Playtest Cards 2021" + + #################################################################### + # Handle General Card Conversions + #################################################################### + + # TODO split collector number on -, TCGPlayer started prefixing list cards with a set code + # TODO The List Reprints -> The List + # TODO remove "- Thick Stock" + + # Remove all Parentheses at the end of cards + row["Name"] = re.sub(r" \(.*\)", "", row["Name"]) + replace_strings(row, config_parser, "NAMES", "Name") + + # We need to do a little extra work on dual faced cards, because deckbox is inconsistent with whether it + # refers to cards by both names or just the front face. + if row["Name"] in scryfall_data: + deckbox_request_url = DECKBOX_URL + urllib.parse.quote( + scryfall_data[row["Name"]] ) - sys.exit() -else: - FILE = sys.argv[1] + with requests.get(deckbox_request_url) as deckbox_response: + + # If we are not redirected to a new page, then we should only use the front face name + deckbox_response_url_parts = urllib.parse.urlparse(deckbox_response.url) + deckbox_response_url = deckbox_response_url_parts._replace(fragment="")._replace(query="").geturl() + + if deckbox_request_url == deckbox_response_url \ + or deckbox_request_url.replace("//", "/") == deckbox_response_url.replace("//", "/"): + print( + "Dual name for '%s' found on deckbox, the dual name will be used for the import." + % (scryfall_data[row["Name"]]) + ) + else: + print( + "Dual name not found for '%s' on deckbox, front face name '%s' will be used." + % (scryfall_data[row["Name"]], row["Name"]) + ) + skip_scryfall_names = True + if not skip_scryfall_names: + row["Name"] = scryfall_data[row["Name"]] -skipcolumns = [ - "Simple Name", - "Set Code", - "Rarity", - "Product ID", - "SKU", - "Price", - "Price Each", -] -outputFile = "deckbox_import.csv" + # Move commander from edition to the end + if "Commander: " in row["Edition"]: + row["Edition"] = re.sub(r"Commander: (.*)$", r"\1 Commander", row["Edition"]) + + # Remove Universes Beyond modifier + if "Universes Beyond: " in row["Edition"]: + row["Edition"] = re.sub(r"Universes Beyond: ", "", row["Edition"]) + + # remove weird symbols from card numbers + row["Card Number"] = re.sub(r"[*★]", "", row["Card Number"]) + + # move Promo Pack to the end if it's not in replacements.config (some old ones actually do have the prefix + if "Promo Pack: " in row["Edition"] and not row["Edition"] in config_parser["EDITIONS"].keys(): + row["Edition"] = re.sub(r"Promo Pack: (.*)$", r"\1 Promo Pack", row["Edition"]) + + # Map Specific Edition Names + replace_strings(row, config_parser, "EDITIONS", "Edition") -configParser = configparser.ConfigParser(delimiters="=") -configParser.read(os.path.join(getPathPrefix(), "replacements.config")) + # write the converted output + writer.writerow(row) -with open(FILE, newline="") as tcgcsvfile, open( - outputFile, "w", newline="" -) as deckboxcsvfile: +def validate_input_csv(tcg_csv_file: TextIO): try: - csv.Sniffer().sniff(tcgcsvfile.read(4096), delimiters=",") - tcgcsvfile.seek(0) - except: - if GUI: - messagebox.showerror( - title="Invalid input file", - message="The file selected does not appear to be a valid CSV file.", - ) - else: - print("The file passed does not appear to be a valid CSV file.") + csv.Sniffer().sniff(tcg_csv_file.read(4096), delimiters=",") + tcg_csv_file.seek(0) + except UnicodeDecodeError: + print("The file passed does not appear to be a valid CSV file.") sys.exit() - csvreader = csv.DictReader(tcgcsvfile) - # Adjust column names - headerstcg = csvreader.fieldnames - for index, header in enumerate(headerstcg): - if header.lower() in configParser["COLUMNS"].keys(): - headerstcg[index] = configParser["COLUMNS"][header.lower()] +def convert(): + load_scryfall_data(MULTI_NAMES_FILE, SCRYFALL_DFC_URL, scryfall_data_func, scryfall_data) + load_scryfall_data(BAB_FILE, SCRYFALL_BAB_URL, scryfall_bab_data_func, scryfall_bab_data) + + # Get our input + input_file = sys.argv[1] + + config_parser = configparser.ConfigParser(delimiters="=") + config_parser.read(os.path.join(get_path_prefix(), "replacements.config")) + + with open(input_file, newline="") as tcg_csv_file, open( + OUTPUT_FILE, "w", newline="" + ) as deckbox_csv_file: + tcg_csv_file: TextIO + deckbox_csv_file: TextIO + validate_input_csv(tcg_csv_file=tcg_csv_file) + + csvreader: DictReader = csv.DictReader(tcg_csv_file) + + # Adjust column names + tcg_headers = csvreader.fieldnames + for index, header in enumerate(tcg_headers): + if header.lower() in config_parser["COLUMNS"].keys(): + tcg_headers[index] = config_parser["COLUMNS"][header.lower()] - # Unnecessary Columns: Simple Name,Set Code,Printing,Rarity,Product ID,SKU,Price,Price Each. - headersdeckbox = [x for x in headerstcg if x not in skipcolumns] + # Unnecessary Columns: Simple Name,Set Code,Printing,Rarity,Product ID,SKU,Price,Price Each. + deckbox_headers: list[str] = [x for x in tcg_headers if x not in SKIP_COLUMNS] - csvwriter = csv.DictWriter( - deckboxcsvfile, quoting=csv.QUOTE_ALL, fieldnames=headersdeckbox + csvwriter: DictWriter = csv.DictWriter( + deckbox_csv_file, quoting=csv.QUOTE_ALL, fieldnames=deckbox_headers + ) + csvwriter.writeheader() + csv_row: dict + for csv_row in csvreader: + process_row(row=csv_row, writer=csvwriter, config_parser=config_parser) + # All Done! + success_msg = "Your import file for deckbox.org is available here: %s" % os.path.abspath( + OUTPUT_FILE ) - csvwriter.writeheader() - for row in csvreader: - skip_scryfall_names = False - - # Don't bother with columns that are going to be ignored anyways - for skippable in skipcolumns: - row.pop(skippable, "") - - # Map the printing column to the Foil column - if row["Foil"] == "Normal": - row["Foil"] = "" - - # Map Card Condition - replace_strings(row, "CONDITIONS", "Condition") - - # Map Chinese Languages - replace_strings(row, "LANGUAGES", "Language") - - #################################################################### - # Map Specific Card Conditions - #################################################################### - - # For BFZ lands...there's no differentiator from the full arts and the non full arts. - row["Name"] = row["Name"].replace(" - Full Art", "") - - # War of the Spark Alternate Arts handled differently - if "(JP Alternate Art)" in row["Name"] and row["Edition"] == "War of the Spark": - row["Edition"] = "War of the Spark Japanese Alternate Art" - row["Name"] = row["Name"].replace(" (JP Alternate Art)", "") - - # Buy a Box Promos worked a little differently with Ixalan - if row["Name"] in ixalan_bab and row["Edition"] == "Buy-A-Box Promos": - row["Edition"] = "Black Friday Treasure Chest Promos" - skip_scryfall_names = True - - # TODO Merge this with above - if row["Name"] in scryfall_bab_data and row["Edition"] == "Buy-A-Box Promos": - if "Promos" in scryfall_bab_data[row["Name"]]: - row["Edition"] = "Media Inserts" - else: - row["Edition"] = scryfall_bab_data[row["Name"]] + print(success_msg) - # Handle Mystery Booster Test Cards, the 2021 release differentiates by Edition - # on deckbox, while tcgplayer differentiates by name appending '(No PW Symbol)' - if ( - "(No PW Symbol)" in row["Name"] - and row["Edition"] == "Mystery Booster: Convention Edition Exclusives" - ): - row["Edition"] = "Mystery Booster Playtest Cards 2021" - - #################################################################### - # Handle General Card Conversions - #################################################################### - - # Remove all Parentheses at the end of cards - row["Name"] = re.sub(r" \(.*\)", "", row["Name"]) - replace_strings(row, "NAMES", "Name") - - # We need to do a little extra work on dual faced cards, because deckbox is inconsistent with whether it refers to cards by both names or just the front face. - if row["Name"] in scryfall_data: - deckbox_request_url = DECKBOX_URL + urllib.parse.quote( - scryfall_data[row["Name"]] - ) - with requests.get(deckbox_request_url) as deckbox_response: - - # If we are not redirected to a new page, then we should only use the front face name - deckbox_response_url_parts = urllib.parse.urlparse(deckbox_response.url) - deckbox_response_url = deckbox_response_url_parts._replace(fragment="")._replace(query="").geturl() - - if deckbox_request_url == deckbox_response_url or deckbox_request_url.replace("//", "/") == deckbox_response_url.replace("//", "/"): - print( - "Dual name for '%s' found on deckbox, the dual name will be used for the import." - % (scryfall_data[row["Name"]]) - ) - else: - print( - "Dual name not found for '%s' on deckbox, front face name '%s' will be used." - % (scryfall_data[row["Name"]], row["Name"]) - ) - skip_scryfall_names = True - if skip_scryfall_names == False: - row["Name"] = scryfall_data[row["Name"]] - - # Fix edition for Buy-A-Box Promos - if row["Name"] in bab_mapping and row["Edition"] == "Buy-A-Box Promos": - row["Edition"] = bab_mapping[row["Name"]] - - # Move commander from edition to the end - if "Commander: " in row["Edition"]: - row["Edition"] = re.sub(r"Commander: (.*)$", r"\1 Commander", row["Edition"]) - - # Remove Universes Beyond modifier - if "Universes Beyond: " in row["Edition"]: - row["Edition"] = re.sub(r"Universes Beyond: ", "", row["Edition"]) - - # remove weird symbols from card numbers - row["Card Number"] = re.sub(r"[*★]", "", row["Card Number"]) - - # move Promo Pack to the end if it's not in replacements.config (some old ones actually do have the prefix - if "Promo Pack: " in row["Edition"] and not row["Edition"] in configParser["EDITONS"].keys(): - row["Edition"] = re.sub(r"Promo Pack: (.*)$", r"\1 Promo Pack", row["Edition"]) - - # Map Specific Edition Names - replace_strings(row, "EDITONS", "Edition") - - # write the converted output - csvwriter.writerow(row) - -# All Done! -successMsg = "Your import file for deckbox.org is available here: %s" % os.path.abspath( - outputFile -) -if GUI: - messagebox.showinfo(title="Conversion completed successfully!", message=successMsg) -else: - print(successMsg) + +convert() From a396abe4adf3c5233b19a07ef3c32ce8365a89f4 Mon Sep 17 00:00:00 2001 From: ThePieBandit <61213359+ThePieBandit@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:52:53 -0400 Subject: [PATCH 17/17] readme and disabling workflows --- .../build.yml | 0 .../release.yml | 0 .idea/codeStyles/codeStyleConfig.xml | 5 ++ README.md | 54 ++++++++++++------- 4 files changed, 39 insertions(+), 20 deletions(-) rename .github/{workflows => workflows-disabled}/build.yml (100%) rename .github/{workflows => workflows-disabled}/release.yml (100%) create mode 100644 .idea/codeStyles/codeStyleConfig.xml diff --git a/.github/workflows/build.yml b/.github/workflows-disabled/build.yml similarity index 100% rename from .github/workflows/build.yml rename to .github/workflows-disabled/build.yml diff --git a/.github/workflows/release.yml b/.github/workflows-disabled/release.yml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows-disabled/release.yml diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 404adac..ffd7b97 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,53 @@ # TCG-to-Deckbox -Python script to convert TCGPlayer app's export file into the proper format for deckbox.org's import. Tested with python 3.6.9 + +Python script to convert TCGPlayer app's export file into the proper format for deckbox.org's import. Tested with python +3.10.12 ## Why this is useful -https://deckbox.org/ is a popular tool for cataloging one's [Magic: the Gathering](https://magic.wizards.com/en) collection. Card entry can be tedious, which is why many users are turning to scanning apps, such as [TCGplayer's card scanning app](https://app.tcgplayer.com/). However, the export format of TCGplayer's app is incompatible with deckbox.org's import format. This tool is designed to reformat the TCGplayer csv export into a valid deckbox csv import. +https://deckbox.org/ is a popular tool for cataloging one's [Magic: the Gathering](https://magic.wizards.com/en) +collection. Card entry can be tedious, which is why many users are turning to scanning apps, such +as [TCGplayer's card scanning app](https://app.tcgplayer.com/). However, the export format of TCGplayer's app is +incompatible with deckbox.org's import format. This tool is designed to reformat the TCGplayer csv export into a valid +deckbox csv import. + +Additionally, this tool now contains some basic replacement logic based on how TCGPlayer and deckbox differ in their +categorization of cards. The short version: TCG player likes to append things like (borderless), (showcase) etc to the +cards, whereas deckbox just uses the collector number. There are two replacement types: generic, and manual. The generic +ones are handled in code, doing things like stripping out parentheses. The specific ones are mostly handled via the +replacements.config file, which lists a mapping of translated values . ## How to use. -The script has two modes: CLI and GUI. To use the CLI, provide the path to your TCGplayer csv file: +To use the CLI, provide the path to your TCGplayer csv file: python3 tcg-to-deckbox.py ~/bin/sample_input.csv -To start it in GUI mode, just launch the program without any arguments +It will give you a new csv, named `deckbox_import.csv` in the directory from which the script was run. - python3 tcg-to-deckbox.py - -In both cases, it will give you a new csv, named `deckbox_import.csv` in the directory from which the script was run. +As part of its process, the script will query https://scryfall.com to get a list of double-faced cards, as these can be +tricky when it comes to matching with deckbox. Additionally, it grabs a list of buy-a-box cards, as those are +categorized completely differently on the TCGPlayer scanner app. The script is configured to only download this once a +week so as not to be excessive. If you have actually scanned any double faced cards, it does double check their name by +querying deckbox.org directly. ## Limitations -1. Card names - I found some card names that are just formatted differently between the two. I manually adjust these card names in my replacements file, however, this was limited to the cards I hit. Of particular note were the Throne of Eldraine Adventure cards, which only used the Creature's name. Another oddity was Tamiyo's journal, which could have different flavor text. If you hit any of these in your collection, you don't need to be a programmer to fix it, just add another entry in `src/replacements.config` and rerun the script. Be sure to let me know so I can add it here too! +1. Card names - I found some card names that are just formatted differently between the two. I manually adjust these + card names in my replacements file, however, this was limited to the cards I hit. Of particular note were the Throne + of Eldraine Adventure cards, which only used the Creature's name. Another oddity was Tamiyo's journal, which could + have different flavor text. If you hit any of these in your collection, you don't need to be a programmer to fix it, + just add another entry in `src/replacements.config` and rerun the script. Be sure to let me know so I can add it here + too! -2. Showcase and extended art cards - TCGplayer suffixes these with `(Extended Art)` or `(Showcase)`. If the script finds these terms, it just deletes them. They're unnecessary as they have different collector's numbers. +2. Showcase and extended art cards - TCGplayer suffixes these with `(Extended Art)` or `(Showcase)`. If the script finds + these terms, it just deletes them. They're unnecessary as they have different collector's numbers. -3. Odd sets from Magic's history - I did my best here, but some of the older sets didn't quite line up. Some I just didn't have enough information on, some looked like categories of multiple sets, and some actually look like they map to multiple different sets in deckbox. If you hit some of these, manual correction is probably the best bet for now, but if you have a solution, feel free to share! +3. Odd sets from Magic's history - I did my best here, but some of the older sets didn't quite line up. Some I just + didn't have enough information on, some looked like categories of multiple sets, and some actually look like they map + to multiple different sets in deckbox. If you hit some of these, manual correction is probably the best bet for now, + but if you have a solution, feel free to share! ## Contributing -When making pull requests please format Python code using Black [black](https://github.com/psf/black) - -```sh -# installing black -pip install black -``` -```sh -# running black -black tcg-to-deckbox.py -``` +Feel free to fork the project and submit suggested fixes