def search_card(cardname, drive_order, grouping=0): # Search for card front and attach it to results cardname_split = None if "&" in cardname: cardname_split = cardname.split("&")[0] results_front = search_card_face(to_searchable(cardname), drive_order, grouping) results = (results_front, ) if not results_front and cardname_split: # Search again but only consider text before the & return search_card(cardname_split, drive_order, grouping) # Determine if this card is a double-faced card # TODO: Not naive string contains for determining if a card is a double faced card? tf_result = [x for x in transforms.keys() if to_searchable(cardname) in x] if tf_result: # Search for the back face, and attach it to results cardname_back = transforms[tf_result[0]] if grouping > 0: results_back = search_card_face(to_searchable(cardname_back), drive_order, grouping + 1) else: results_back = search_card_face(to_searchable(cardname_back), drive_order) if results_back: results = (results_front, results_back) if not results_front: results = ({"query": cardname}, ) return results
def sync_dfcs(): scryfall_query = "https://api.scryfall.com/cards/search?q=is:dfc%20-layout:art_series%20-layout:double_faced_token" response = json.loads(requests.get(scryfall_query).content) # maintain list of all dfcs found so far q_dfcpairs = [] for x in response['data']: # retrieve front and back names for this card, then create a DFCPair for it and append to list front_name = x['card_faces'][0]['name'] back_name = x['card_faces'][1]['name'] q_dfcpairs.append( DFCPair(front=to_searchable(front_name), back=to_searchable(back_name))) # synchronise the located DFCPairs to database t0 = time.time() key_fields = ('front', ) ret = bulk_sync(new_models=q_dfcpairs, key_fields=key_fields, filters=None, db_class=DFCPair) print( "Finished synchronising database with Scryfall DFCs, which took {} seconds." .format(time.time() - t0))
def parse_text(input_lines, offset=0): transforms = dict((x.front, x.back) for x in DFCPair.objects.all()) cards_dict = OrderDict() curr_slot = offset # loop over lines in the input text, and for each, parse it into usable information for line in input_lines.splitlines(): # extract the query and quantity from the current line of the input text (query, qty) = process_line(line) if query: # cap at 612 over_cap = False if qty + curr_slot >= 612: qty = 612 - curr_slot over_cap = True req_type = "" curr_slots = list(range(curr_slot, curr_slot + qty)) # first, determine if this card is a DFC by virtue of it having its two faces separated by an ampersand query_faces = [query, ""] if "&" in query_faces[0]: query_split = [to_searchable(x) for x in query.split(" & ")] if (query_split[0] in transforms.keys() and query_split[1] in transforms.values()): query_faces = query_split elif query[0:2].lower() == "t:": query_faces[0] = to_searchable(query[2:]) req_type = "token" else: query_faces[0] = to_searchable(query) # gotta check if query is the front of a DFC here as well if query_faces[0] in transforms.keys(): query_faces = [query, transforms[query_faces[0]]] # stick the front face into the dictionary cards_dict.insert(query_faces[0], curr_slots, "front", req_type, "") if query_faces[1]: # is a DFC, gotta add the back face to the correct slots cards_dict.insert(query_faces[1], curr_slots, "back", req_type, "") else: # is not a DFC, so add this card's slots onto the common cardback's slots cards_dict.insert_empty(curr_slots, "back") curr_slot += qty if over_cap: break return cards_dict.order, curr_slot - offset
def search_database(drive_order, query, s): # TODO: elasticsearch_dsl.serializer.serializer ? # search through the database for a given query, over the drives specified in drive_orders, # using the search index specified in s (this enables reuse of code between Card and Token search functions) results = [] # set up search - match the query and use the AND operator match = Match(searchq={"query": to_searchable(query), "operator": "AND"}) # match the cardname once instead of for every drive to save on search time s_query = s.query(match) # iterate over drives, filtering on the current drive, ordering by priority in descending order, # then add the returned hits to the results list hits = s_query.sort({ 'priority': { 'order': 'desc' } }).params(preserve_order=True).scan() results0 = [x.to_dict() for x in hits] for drive in drive_order: results += [x for x in results0 if x['source'] == drive] return results
def search(drive_order, query, req_type): # this function can either receive a request with "normal" type with query like "t:goblin", # or a request with "token" type with query like "goblin", so handle both of those cases here if query.lower()[0:2] == "t:": query = query[2:] req_type = "token" # now that we've potentially trimmed the query for tokens, convert the query to a searchable string query = to_searchable(query) # search for tokens if this request is for a token if req_type == "token": results = query_es_token(drive_order, query) # search for cardbacks if request is for cardbacks elif req_type == "back": results = query_es_cardback() # otherwise, search normally else: results = query_es_card(drive_order, query) return { "data": results, "req_type": req_type, "query": query, }
def sync_dfcs(): scryfall_query_dfc = "https://api.scryfall.com/cards/search?q=is:dfc%20-layout:art_series%20-layout:double_faced_token" response_dfc = json.loads(requests.get(scryfall_query_dfc).content) # maintain list of all dfcs found so far q_dfcpairs = [] for x in response_dfc["data"]: # retrieve front and back names for this card, then create a DFCPair for it and append to list front_name = x["card_faces"][0]["name"] back_name = x["card_faces"][1]["name"] q_dfcpairs.append( DFCPair(front=to_searchable(front_name), back=to_searchable(back_name))) # also retrieve meld pairs and save them as DFCPairs time.sleep(0.1) scryfall_query_meld = "https://api.scryfall.com/cards/search?q=is:meld%" response_meld = json.loads(requests.get(scryfall_query_meld).content) for x in response_meld["data"]: card_part = [y for y in x["all_parts"] if y["name"] == x["name"]][0] meld_result = [ y for y in x["all_parts"] if y["component"] == "meld_result" ][0]["name"] if card_part["component"] == "meld_part": is_top = "\n(Melds with " not in x["oracle_text"] card_bit = "Top" if is_top else "Bottom" q_dfcpairs.append( DFCPair( front=to_searchable(x["name"]), back=to_searchable(f"{meld_result} {card_bit}"), )) # synchronise the located DFCPairs to database t0 = time.time() key_fields = ("front", ) ret = bulk_sync(new_models=q_dfcpairs, key_fields=key_fields, filters=None, db_class=DFCPair) print( "Finished synchronising database with Scryfall DFCs, which took {} seconds." .format(time.time() - t0))
def add_card(folderDict, parentDict, folder, db_datetime, item): if not item['trashed']: folderName = folderDict[item['parents'][0]] parentName = parentDict[item['parents'][0]] owner = item['owners'][0]['displayName'] scryfall = False priority = 2 if "Retro Cube" in parentName: priority = 0 if ")" in item['name']: priority = 1 source = "Unknown" if folder['name'] == "Chilli_Axe's MPC Proxies": source = "Chilli_Axe" if folderName == "12. Cardbacks": if "Black Lotus" in item['name']: priority += 10 source += "_cardbacks" priority += 5 elif owner in OWNERS: source = OWNERS[owner] elif folder['name'] == "MPC Scryfall Scans": source = "berndt_toast83/" + folderName scryfall = True if "Basic" in folderName: priority += 5 if scryfall: SOURCES["berndt_toast83"]["quantity"] += 1 else: SOURCES[source]["quantity"] += 1 # Store the image's static URL static_url = "https://drive.google.com/thumbnail?sz=w400-h400&id=" + item[ 'id'] # Calculate source image DPI, rounded to tens dpi = 10 * round( int(item['imageMediaMetadata']['height']) * DPI_HEIGHT_RATIO / 10) # Return card info so we can insert into database return item['id'], cardname, priority, source, dpi, to_searchable( cardname), static_url
def search_database(drive_order, query, model): if query == "": return [] # search through the database for a given query, over the drives specified in drive_orders, # using the search index specified in s (this enables reuse of code between Card and Token search functions) results = [] query_parsed = to_searchable(query) hits = [ x.to_dict() for x in model.objects.filter(searchq__search=query_parsed) ] hits.sort(key=lambda x: distance(x["searchq"], query_parsed)) for drive in drive_order: results += [x for x in hits if x["source"] == drive] return results
'Village Ironsmith': 'Ironfang', 'Village Messenger': 'Moonrise Intruder', 'Villagers of Estwald': 'Howlpack of Estwald', 'Voldaren Pariah': 'Abolisher of Bloodlines', 'Westvale Abbey': 'Ormendahl, Profane Prince', 'Wolfbitten Captive': 'Krallenhorde Killer', 'Gisela, the Broken Blade': 'Brisela, Voice of Nightmares Top', 'Bruna, the Fading Light': 'Brisela, Voice of Nightmares Bottom', 'Hanweir Battlements': 'Hanweir, the Writhing Township Top', 'Hanweir Garrison': 'Hanweir, the Writhing Township Bottom', 'Graf Rats': 'Chittering Host Top', 'Midnight Scavengers': 'Chittering Host Bottom' } transforms = dict( (to_searchable(x), to_searchable(y)) for x, y in transforms.items()) def search_card_face(cardname, drive_order, grouping=0): # Search for a card, given its name and the drives to search # Return a tuple of dictionaries (returning as a dict rather than Card for Javascript access purposes) results = SearchQuerySet().filter(content=cardname) # Retrieve Card objects from search results while filtering out cardbacks card_objs = [ x.object for x in results if "_cardback" not in x.object.source ] cards_found = [] for source in drive_order: cards_this_source = [x for x in card_objs if source in x.source] if cards_this_source: # Sort cards from this source by priority and convert them all to dicts
def parse_csv(csv_bytes): # TODO: I'm sure this can be cleaned up a lot, the logic here is confusing and unintuitive cards_dict = OrderDict() curr_slot = 0 # support for different types of encoding - detect the encoding type then decode the given bytes according to that csv_format = chardet.detect(csv_bytes) csv_string_split = csv_bytes.decode(csv_format['encoding']).splitlines() # handle case where csv doesn't have correct headers headers = 'Quantity,Front,Back' if csv_string_split[0] != headers: # this CSV doesn't appear to have the correct column headers, so we'll attach them here csv_string_split = [headers] + csv_string_split csv_dictreader = csv.DictReader(csv_string_split) for line in csv_dictreader: qty = line['Quantity'] if qty: # try to parse qty as int try: qty = int(qty) except ValueError: # invalid qty continue else: # for empty quantities, assume qty=1 qty = 1 # only care about lines with a front specified if line['Front']: # the slots for this line in the CSV curr_slots = list(range(curr_slot, curr_slot + qty)) query_faces = [line['Front'], line['Back']] req_type_front = "normal" req_type_back = "normal" # process the front face as a token if necessary if query_faces[0][0:2].lower() == "t:": query_faces[0] = query_faces[0][2:] req_type_front = "token" if not line['Back']: # back face not specified # potentially doing transform things, because a back wasn't specified # first, determine if this card is a DFC by virtue of it having its two faces separated by an ampersand if '&' in query_faces[0]: query_split = [ to_searchable(x) for x in query.split(" & ") ] if query_split[0] in transforms.keys( ) and query_split[1] in transforms.values(): query_faces = query_split else: # gotta check if query is the front of a DFC here as well query_faces[0] = to_searchable(query_faces[0]) if query_faces[0] in transforms.keys(): query_faces = [ query_faces[0], transforms[query_faces[0]] ] else: # both sides specified - process the back face as a token if necessary if query_faces[1][0:2].lower() == "t:": query_faces[1] = query_faces[1][2:] req_type_back = "token" # ensure everything has been converted to searchable query_faces = [to_searchable(x) for x in query_faces] # stick the front face into the dictionary cards_dict.insert(query_faces[0], curr_slots, "front", req_type_front, "") if query_faces[1]: # is a DFC, gotta add the back face to the correct slots cards_dict.insert(query_faces[1], curr_slots, "back", req_type_back, "") else: # is not a DFC, so add this card's slots onto the common cardback's slots cards_dict.insert_back(curr_slots) curr_slot += qty # TODO: Read in chunks if big? return cards_dict.order, curr_slot
def add_card(folderDict, parentDict, folder, item): try: # file is valid when it's not trashed and filesize does not exceed 30 MB valid = not item['trashed'] and int(item['size']) < 30000000 if not valid: print("Can't index this card: {}".format(item)) except KeyError: valid = True if valid: # strip the extension off of the item name to retrieve the card name try: [cardname, extension] = item['name'].rsplit('.', 1) except ValueError: print("nani {}".format(item['name'])) return img_type = "normal" folderName = folderDict[item['parents'][0]] parentName = parentDict[item['parents'][0]] owner = item['owners'][0]['displayName'] scryfall = False priority = 2 if "Retro Cube" in parentName: priority = 0 if ")" in cardname: priority = 1 source = "Unknown" if folder['name'] == "Chilli_Axe's MPC Proxies": source = "Chilli_Axe" if folderName == "12. Cardbacks": if "Black Lotus" in item['name']: priority += 10 img_type = "cardback" priority += 5 elif folder['name'] == "nofacej MPC Card Backs": img_type = "cardback" source = "nofacej" # this elif and the next one were the other way around - swap them back if shit breaks elif folder['name'] == "MPC Scryfall Scans": source = "berndt_toast83/" + folderName scryfall = True elif owner in OWNERS: source = OWNERS[owner] if "basic" in folderName.lower(): priority += 5 elif "token" in folderName.lower(): img_type = "token" elif "cardbacks" in folderName.lower( ) or "card backs" in folderName.lower(): img_type = "cardback" # Store the image's static URL static_url = "https://drive.google.com/thumbnail?sz=w400-h400&id=" + item[ 'id'] # Calculate source image DPI, rounded to tens dpi = 10 * round( int(item['imageMediaMetadata']['height']) * DPI_HEIGHT_RATIO / 10) # Return card info so we can insert into database, in the correct list card_info = (item['id'], cardname, priority, source, dpi, to_searchable(cardname), extension, item['createdTime']) # Skip card if its source couldn't be determined if source == "Unknown": return if img_type == "cardback": q_cardbacks.append(card_info) if scryfall: SOURCES["berndt_toast83"]["qty_cardbacks"] += 1 else: SOURCES[source]["qty_cardbacks"] += 1 elif img_type == "token": q_tokens.append(card_info) if scryfall: SOURCES["berndt_toast83"]["qty_tokens"] += 1 else: SOURCES[source]["qty_tokens"] += 1 else: q_cards.append(card_info) if scryfall: SOURCES["berndt_toast83"]["qty_cards"] += 1 else: SOURCES[source]["qty_cards"] += 1
def add_card(folder, source, item, q_cards, q_cardbacks, q_tokens): try: # file is valid when it's not trashed and filesize does not exceed 30 MB valid = not item["trashed"] and int(item["size"]) < 30000000 if not valid: print( "Can't index this card: <{}> {}, size: {} bytes".format( item["id"], item["name"], item["size"] ) ) except KeyError: valid = True if valid: # strip the extension off of the item name to retrieve the card name try: [cardname, extension] = item["name"].rsplit(".", 1) except ValueError: print("Issue with parsing image: {}".format(item["name"])) return source_verbose = "Unknown" img_type = "card" folder_name = item["folder_name"] parent_name = item["parent_name"] # Skip this file if it doesn't have a name after splitting name & extension if not cardname: return scryfall = False priority = 2 if ")" in cardname: priority = 1 if folder["name"] == "Chilli_Axe's MPC Proxies": source_verbose = "Chilli_Axe" if "Retro Cube" in parent_name: priority = 0 source_verbose = "Chilli_Axe Retro Cube" elif folder_name == "12. Cardbacks": if "Black Lotus" in item["name"]: priority += 10 img_type = "cardback" priority += 5 elif folder["name"] == "nofacej MPC Card Backs": img_type = "cardback" source_verbose = source.id elif folder["name"] == "MPC Scryfall Scans": source_verbose = "berndt_toast83/" + folder_name scryfall = True else: source_verbose = source.id if "basic" in folder_name.lower(): priority += 5 source_verbose = source_verbose + " Basics" elif "token" in folder_name.lower(): img_type = "token" if not scryfall: source_verbose = source_verbose + " Tokens" elif "cardbacks" in folder_name.lower() or "card backs" in folder_name.lower(): img_type = "cardback" source_verbose = source_verbose + " Cardbacks" # Calculate source image DPI, rounded to tens dpi = 10 * round( int(item["imageMediaMetadata"]["height"]) * DPI_HEIGHT_RATIO / 10 ) # Skip card if its source couldn't be determined if source == "Unknown": return # Use a dictionary to map the img type to the queue and class of card we need to append/create queue_object_map = { "cardback": (q_cardbacks, Cardback), "token": (q_tokens, Token), "card": (q_cards, Card), } queue_object_map[img_type][0].append( queue_object_map[img_type][1]( id=item["id"], name=cardname, priority=priority, source=source, source_verbose=source_verbose, dpi=dpi, searchq=to_searchable(cardname), thumbpath=extension, date=item["createdTime"], ) )
def add_card(folderDict, parentDict, folder, db_datetime, item): if not item['trashed']: folderName = folderDict[item['parents'][0]] parentName = parentDict[item['parents'][0]] owner = item['owners'][0]['displayName'] folders_sources = { "Jake Rowe": "nofacej_cardbacks", "Bazuki Alters": "Bazukii", "Karlin Courtney": "hathwellcrisping", "Digital Red": "Proxycommander", "Alastair Jack": "male_MPC", "i Derp": "iDerp69", "Trey Kapfer": "MrChow1917", "Tristan DELMAS": "Celid_of_the_wind" } scryfall = False priority = 2 if "Retro Cube" in parentName: priority = 0 if ")" in item['name']: priority = 1 source = "Unknown" if folder['name'] == "Chilli_Axe's MPC Proxies": source = "Chilli_Axe" if folderName == "12. Cardbacks": if "Black Lotus" in item['name']: priority += 10 source += "_cardbacks" priority += 5 elif owner in folders_sources.keys(): source = folders_sources[owner] elif folder['name'] == "MPC Scryfall Scans": source = "berndt_toast83/" + folderName scryfall = True if "Basic" in folderName: priority += 5 if scryfall: SOURCES["berndt_toast83"]["quantity"] += 1 else: SOURCES[source]["quantity"] += 1 folder_path = "./../staticroot/cardpicker/" + source # folder_path = "cardpicker/static/cardpicker/" + source folder_path = os.path.abspath(folder_path) # Download card thumbnail if necessary file_datetime = datetime.datetime.strptime(item["modifiedTime"], "%Y-%m-%dT%H:%M:%S.%fZ") thumbnail_path = folder_path + "/" + item['id'] + ".png" try: if not os.path.exists(folder_path): os.makedirs(folder_path) except FileExistsError: pass # Calculate source image DPI, rounded to tens dpi = 10 * round( int(item['imageMediaMetadata']['height']) * DPI_HEIGHT_RATIO / 10) if not os.path.isfile(thumbnail_path) or file_datetime > db_datetime: # three tries at downloading the file counter = 0 while counter < 3: try: # Read thumbnail thumbnail = imageio.imread( "https://drive.google.com/thumbnail?sz=w400-h400&id=" + item['id']) # Trim off 13 pixels around the edges, which should remove the print bleed edge, # assuming the image is 293 x 400 in resolution, before writing to disk imageio.imwrite(thumbnail_path, thumbnail[13:-13, 13:-13, :]) break except: # TODO: Not bare except counter += 1 if counter >= 3: print("Failed to download thumbnail for: {}".format( item['name'])) # Remove the file extension from card name cardname = '.'.join(item['name'].split(".")[0:-1]) # Store the image's static URL as well static_url = "cardpicker/" + source + "/" + item['id'] + ".png" # Return card info so we can insert into database return item['id'], cardname, priority, source, dpi, to_searchable( cardname), static_url
'Sea Gate Restoration': 'Sea Gate, Reborn', 'Sejiri Shelter': 'Sejiri Glacier', 'Shatterskull Smashing': 'Shatterskull, the Hammer Pass', 'Silundi Vision': 'Silundi Isle', 'Skyclave Cleric': 'Skyclave Basilica', 'Song-Mad Treachery': 'Song-Mad Ruins', 'Spikefield Hazard': 'Spikefield Cave', 'Tangled Florahedron': 'Tangled Vale', 'Turntimber Symbiosis': 'Turntimber, Serpentine Wood', 'Umara Wizard': 'Umara Skyfalls', 'Valakut Awakening': 'Valakut Stoneforge', 'Vastwood Fortification': 'Vastwood Thicket', 'Zof Consumption': 'Zof Bloodbog', } transforms = dict((to_searchable(x), to_searchable(y)) for x, y in transforms.items()) def search_card_face(cardname, drive_order, grouping=0): # Search for a card, given its name and the drives to search # Return a tuple of dictionaries (returning as a dict rather than Card for Javascript access purposes) results = SearchQuerySet().filter(content=cardname).load_all() # Retrieve Card objects from search results while filtering out cardbacks card_objs = [x.object for x in results if "_cardback" not in x.object.source] cards_found = [] for source in drive_order: cards_this_source = [x for x in card_objs if source in x.source] if cards_this_source: # Sort cards from this source by priority and convert them all to dicts priorities = [int(x.priority) for x in cards_this_source] cards_this_source = [x.to_dict() for _, x in sorted(zip(priorities, cards_this_source),