import copy import enum import json import jsonpickle import os import re import sys import six.moves.cPickle as pickle from time import localtime, strftime, time from PIL import Image as PImage import urllib.request from urllib import request from mtgsdk import Set, Card, MtgException import gi gi.require_version('Gtk', '3.0') from gi.repository import GdkPixbuf, GLib # Title of the Program Window APPLICATION_TITLE = "Card Vault" # Program version VERSION = "0.6.0" # Path of image cache CACHE_PATH = os.path.expanduser('~') + "/.cardvault/" IMAGE_CACHE_PATH = os.path.expanduser('~') + "/.cardvault/images/" ICON_CACHE_PATH = os.path.expanduser('~') + "/.cardvault/icons/" # Log level of the application # 1 Info # 2 Warning # 3 Error LOG_LEVEL = 1 # Name of the database DB_NAME = "cardvault.db" ALL_NUM_URL = 'https://api.magicthegathering.io/v1/cards?page=0&pageSize=100' ALL_SETS_JSON_URL = 'https://mtgjson.com/json/AllSets-x.json' # URL for card images. Insert card.multiverse_id. CARD_IMAGE_URL = 'http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid={}&type=card' # Location of manual wiki MANUAL_LOCATION = 'https://github.com/luxick/cardvault' # Colors for card rows in search view SEARCH_TREE_COLORS = { "unowned": "black", "wanted": "#D39F30", "owned": "#62B62F" } # Colors for card rows in every default view GENERIC_TREE_COLORS = { "unowned": "black", "wanted": "black", "owned": "black" } LEGALITY_COLORS = { "Banned": "#C65642", "Restricted": "#D39F30", "Legal": "#62B62F" } DEFAULT_CONFIG = { "last_viewed": "", "show_all_in_search": True, "start_page": "search", "local_db": False, "first_run": True, "log_level": 3 } card_colors = { 'White': 'W', 'Blue': 'U', 'Black': 'B', 'Red': 'R', 'Green': 'G' } color_sort_order = { 'W': 0, 'U': 1, 'B': 2, 'R': 3, 'G': 4 } rarity_dict = { "special": 0, "common": 1, "uncommon": 2, "rare": 3, "mythic rare": 4 } card_types = ["Creature", "Artifact", "Instant", "Enchantment", "Sorcery", "Land", "Planeswalker"] online_icons = { True: 'network-wired', False: 'drive-harddisk' } online_tooltips = { True: 'Using online card data', False: 'Using card data from local database.' } class LogLevel(enum.Enum): Error = 1 Warning = 2 Info = 3 class TerminalColors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' def log(msg: str, ll: LogLevel): if ll.value <= LOG_LEVEL: lv = "[" + ll.name + "] " if ll.value == 2: c = TerminalColors.WARNING elif ll.value == 1: c = TerminalColors.BOLD + TerminalColors.FAIL else: c = "" tc = strftime("%H:%M:%S ", localtime()) print(c + lv + tc + msg + TerminalColors.ENDC) def parse_config(filename: str, default: dict): config = copy.copy(default) try: with open(filename) as configfile: loaded_config = json.load(configfile) config.update(loaded_config) except IOError: # Will just use the default config # and create the file for manual editing save_config(config, filename) except ValueError: # There's a syntax error in the config file log("Syntax error wihle parsing config file", LogLevel.Error) return return config def save_config(config: dict, filename: str): path = os.path.dirname(filename) if not os.path.isdir(path): os.mkdir(path) with open(filename, 'wb') as configfile: configfile.write(json.dumps(config, sort_keys=True, indent=4, separators=(',', ': ')).encode('utf-8')) def resource_path(relative_path): if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath("."), relative_path) def get_root_filename(filename: str) -> str: return os.path.expanduser(os.path.join('~', '.cardvault', filename)) def get_ui_filename(filename: str) -> str: return os.path.join(os.path.dirname(__file__), 'gui', filename) def reload_image_cache(path: str) -> dict: cache = {} if not os.path.isdir(path): os.mkdir(path) imagefiles = os.listdir(path) for imagefile in imagefiles: try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(path + imagefile) # Strip filename extension imagename = os.path.splitext(imagefile)[0] cache[int(imagename)] = pixbuf except OSError as err: log("Error loading image: " + str(err), LogLevel.Error) except GLib.GError as err: log("Error loading image: " + str(err), LogLevel.Error) return cache def reload_preconstructed_icons(path: str) -> dict: cache = {} if not os.path.exists(path): os.makedirs(path) files = os.listdir(path) for file in files: # Split filename into single icon names and remove extension without_ext = file.split(".")[0] names = without_ext.split("_") # Compute size of the finished icon pic_width = len(names) * 105 pic_height = 105 try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(ICON_CACHE_PATH + file) pixbuf = pixbuf.scale_simple(pic_width / 5, pic_height / 5, GdkPixbuf.InterpType.HYPER) # Set name for icon iconname = "_".join(names) cache[iconname] = pixbuf except OSError as err: log("Error loading image: " + str(err), LogLevel.Error) return cache def load_mana_icons(path: str) -> dict: if not os.path.exists(path): log("Directory for mana icons not found " + path, LogLevel.Error) return {} icons = {} files = os.listdir(path) for file in files: img = PImage.open(path + file) # Strip file extension name = os.path.splitext(file)[0] icons[name] = img return icons def net_all_cards_mtgjson() -> dict: with urllib.request.urlopen(ALL_SETS_JSON_URL) as url: data = json.loads(url.read().decode()) return data def net_load_set_list() -> dict: """ Load the list of all MTG sets from the Gather""" try: start = time() sets = Set.all() stop = time() log("Fetched set list in {}s".format(round(stop - start, 3)), LogLevel.Info) except MtgException as err: log(str(err), LogLevel.Error) return {} return [set.__dict__ for set in sets] def load_sets(filename: str) -> dict: """ Load sets from local file if possible. Called by: Application if in online mode """ if not os.path.isfile(filename): # use mtgsdk api to retrieve al list of all sets sets = net_load_set_list() # Serialize the loaded data to a file pickle.dump(sets, open(filename, 'wb')) # Deserialize set data from local file sets = pickle.load(open(filename, 'rb')) # Sort the loaded sets based on the sets name output = {} for set in sorted(sets, key=lambda x: x.get('name')): output[set.get('code')] = set return output def export_library(path, file): try: pickle.dump(file, open(path, 'wb')) log("Library exported to \"" + path + "\"", LogLevel.Info) except OSError as err: log(str(err), LogLevel.Error) def export_json(path, file): """Write file in json format""" try: f = open(path, 'w') s = jsonpickle.encode(file) f.write(s) except OSError as err: log(str(err), LogLevel.Error) def import_library(path: str) -> (): try: imported = pickle.load(open(path, 'rb')) except pickle.UnpicklingError as err: log(str(err) + " while importing", LogLevel.Error) return # Parse imported file try: library = imported["library"] tags = imported["tags"] wants = imported["wants"] except KeyError as err: log("Invalid library format " + str(err), LogLevel.Error) library = {} tags = {} wants = {} log("Library imported", LogLevel.Info) return library, tags, wants def save_file(path, file): # Serialize using cPickle try: pickle.dump(file, open(path, 'wb')) except OSError as err: log(str(err), LogLevel.Error) return log("Saved file " + path, LogLevel.Info) def load_file(path: str): if not os.path.isfile(path): log(path + " does not exist", LogLevel.Warning) return try: loaded = pickle.load(open(path, 'rb')) except OSError as err: log(str(err), LogLevel.Error) return return loaded def load_dummy_image(size_x: int, size_y: int) -> GdkPixbuf: return GdkPixbuf.Pixbuf.new_from_file_at_size(os.path.dirname(__file__) + '/resources/images/dummy.jpg', size_x, size_y) def load_card_image(card: dict, size_x: int, size_y: int, cache: dict) -> GdkPixbuf: """ Retrieve an card image from cache or alternatively load from gatherer""" try: image = cache[card.get('multiverse_id')] except KeyError: log("No local image for " + card.get('name') + ". Loading from " + card.get('image_url'), LogLevel.Info) filename, image = net_load_card_image(card, size_x, size_y) cache[card.get('multiverse_id')] = image return image def net_load_card_image(card: dict, size_x: int, size_y: int) -> (str, GdkPixbuf): url = card.get('image_url') if url is None: log("No Image URL for " + card.get('name'), LogLevel.Warning) return load_dummy_image(size_x, size_y) filename = IMAGE_CACHE_PATH + str(card.get('multiverse_id')) + ".png" request.urlretrieve(url, filename) return filename, GdkPixbuf.Pixbuf.new_from_file_at_size(filename, size_x, size_y) def create_mana_icons(icons: dict, mana_string: str) -> GdkPixbuf: # Convert the string to a List safe_string = mana_string.replace("/", "-") glyphs = re.findall("{(.*?)}", safe_string) if len(glyphs) == 0: return # Compute horizontal size for the final image size = len(glyphs) * 105 image = PImage.new("RGBA", (size, 105)) # Increment for each position of an icon # (Workaround: 2 or more of the same icon will be rendered in the same position) c = 0 # Go through all entries an add the correspondent icon to the final image for icon in glyphs: x_pos = c * 105 try: loaded = icons[icon] except KeyError: log("No icon file named '" + icon + "' found.", LogLevel.Warning) return image.paste(loaded, (x_pos, 0)) c += 1 # Save Icon file path = ICON_CACHE_PATH + "_".join(glyphs) + ".png" image.save(path) try: pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) pixbuf = pixbuf.scale_simple(image.width / 5, image.height / 5, GdkPixbuf.InterpType.HYPER) except: return return pixbuf def unique_list(seq): seen = set() seen_add = seen.add return [x for x in seq if not (x in seen or seen_add(x))] def sizeof_fmt(num, suffix='B'): for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 return "%.1f%s%s" % (num, 'Yi', suffix) def get_all_cards_num() -> int: req = urllib.request.Request(ALL_NUM_URL, headers={'User-Agent': 'Mozilla/5.0'}) response = urllib.request.urlopen(req) headers = response.info()._headers for header, value in headers: if header == 'Total-Count': return int(value)