421 lines
12 KiB
Python
421 lines
12 KiB
Python
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)
|