This is an old revision of the document!
Table of Contents
Card Lookup
A Python3 script to look up cards hiding behind a numbered reference (such as kanan4
or you5
) or by attribute/type (such as ssk kanan
or avo you
) using the Kirara database. The result (or error message) can also be opened directly in a web browser tab, if this script is run as a shortcut using something like dmenu.
Usage
Download the script below (either by copypasting or by clicking the file name above the code block), make sure Python3 is installed, then run a lookup by passing the search as an argument (see Examples below). If you want to run the script via dmenu, make sure the script or a link to it is placed in your PATH.
The script optionally uses the python-Levenshtein module to deal with typos in the idol name. If it is not installed, only exact name matches can be looked up.
The script is made for UNIX (Linux/Mac/etc), Windows users will have to change the paths for the temporary files in lines 8 and 9. As for running the script as a shortcut, Windows users could probably do a thing with AutoHotkey or something? Open a text input when hitting a hotkey, then put that input into the script as argument?
Examples
# Requests are *not* case-sensitive ./sifas-lookup.py maki3 # Look up Maki's third UR ./sifas-lookup.py nozomi # Look up all of Nozomi's URs ./sifas-lookup.py pure vo chika # Look up Chika's [Pure] [Vo]-type URs ./sifas-lookup.py purevo chika ./sifas-lookup.py p vo chika ./sifas-lookup.py pvo chika ./sifas-lookup.py e pomu # Look up Ayumu's [E]legant URs ./sifas-lookup.py elegant ayumu ./sifas-lookup.py sp ai # Look up Ai's [Sp]-type URs
Script
- sifas-lookup.py
- #!/usr/bin/env python3
- import json, os, re, stat, sys, time, urllib.request, webbrowser
- ###
- # FEEL FREE TO TOUCH THIS
- ###
- CACHEDIR = os.path.expanduser("~/.cache/sifas-lookup")
- TMPFILE = "/tmp/sifas-lookup-response"
- namemap = {
- 1: ["honoka", "honk"],
- 2: ["eli"],
- 3: ["kotori", "koto", "bird"],
- 4: ["umi"],
- 5: ["rin"],
- 6: ["maki"],
- 7: ["nozomi", "nozo"],
- 8: ["hanayo", "pana"],
- 9: ["nico"],
- 101: ["chika"],
- 102: ["riko"],
- 103: ["kanan", "hagu"],
- 104: ["dia"],
- 105: ["you"],
- 106: ["yoshiko", "yoshi", "yohane", "yoha"],
- 107: ["hanamaru", "maru", "zuramaru", "zura"],
- 108: ["mari"],
- 109: ["ruby"],
- 201: ["ayumu", "pomu"],
- 202: ["kasumi", "kasumin", "kasu", "kasukasu"],
- 203: ["shizuku", "shizu"],
- 204: ["karin"],
- 205: ["ai"],
- 206: ["kanata"],
- 207: ["setsuna", "setsu", "nana"],
- 208: ["emma"],
- 209: ["rina"],
- 210: ["shioriko", "shio"]
- }
- ###
- # ENTERING "PROBABLY DON'T WANT TO TOUCH" ZONE
- ###
- debug_in_terminal = False
- member_ids = namemap.keys()
- namemap = {n:i for (i,nl) in namemap.items() for n in nl}
- number_regex = re.compile("^([A-Za-z]+) ?(\d+)$")
- class RequestError(Exception):
- pass
- class CardLookupError(Exception):
- pass
- # Functions to convert string to IDs
- def short_attr_to_id(short_attr):
- if short_attr == "s": return 1
- if short_attr == "p": return 2
- if short_attr == "c": return 3
- if short_attr == "a": return 4
- if short_attr == "n": return 5
- if short_attr == "e": return 6
- raise RequestError("Unknown attribute name: " + str(short_attr))
- def role_to_id(role):
- if role == "vo": return 1
- if role == "sp": return 2
- if role == "gd": return 3
- if role == "sk": return 4
- raise RequestError("Unknown role name: " + str(role))
- # Functions for user-readable strings
- def card_summary(card):
- if card["attribute"] == 1: attr = "Smile"
- elif card["attribute"] == 2: attr = "Pure"
- elif card["attribute"] == 3: attr = "Cool"
- elif card["attribute"] == 4: attr = "Active"
- elif card["attribute"] == 5: attr = "Natural"
- elif card["attribute"] == 6: attr = "Elegant"
- if card["role"] == 1: role = "Vo"
- elif card["role"] == 2: role = "Sp"
- elif card["role"] == 3: role = "Gd"
- elif card["role"] == 4: role = "Sk"
- return "#" + str(card["ordinal"]) + " " + attr + " " + role + " (" + card["normal_appearance"]["name"] + " / " + card["idolized_appearance"]["name"] + ")"
- def card_url(card):
- return "https://allstars.kirara.ca/card/" + str(card["ordinal"])
- # Functions to find matching cards in an API response
- def get_card_by_number(data, number):
- for card in reversed(data["result"]):
- if card["rarity"] != 30: continue
- number -= 1
- if number <= 0: return [card]
- return []
- def get_cards_by_attribute_and_role(data, attribute, role):
- cards = []
- for card in reversed(data["result"]):
- if card["rarity"] != 30 or (attribute != None and card["attribute"] != attribute) or (role != None and card["role"] != role): continue
- cards.append(card)
- return cards
- # Dealing with user input
- def do_lookup(request):
- global namemap, number_regex
- membername = None
- number = None
- attribute = None
- role = None
- # Parse arguments
- m = number_regex.match(" ".join(request))
- if m != None:
- # Lookup by Number
- membername = m.group(1).lower()
- number = int(m.group(2))
- else:
- # Lookup by Attribute/Type/both/neither
- membername = request[-1].lower()
- if len(request) == 3:
- # [attr] [role] [name]
- attribute = short_attr_to_id(request[0][0].lower())
- role = role_to_id(request[1].lower())
- elif len(request) == 2:
- if len(request[0]) == 3:
- # [short_attr][role] [name]
- attribute = short_attr_to_id(request[0][0].lower())
- role = role_to_id(request[0][1:].lower())
- elif len(request[0]) == 2:
- # [role] [name]
- role = role_to_id(request[0].lower())
- else:
- # [attr][role?] [name]
- try:
- role = role_to_id(request[0][-2:])
- except RequestError:
- role = None
- attribute = short_attr_to_id(request[0][0].lower())
- elif len(request) != 1:
- # No or too many arguments
- raise RequestError("Unable to interpret request: " + " ".join(request))
- # If there's exactly one argument, it's a name-only request
- # get member ID from requested name
- if membername not in namemap:
- try:
- from Levenshtein import jaro_winkler
- bestdist = 0.9 # minimum similarity to meet
- bestname = None
- for name in namemap.keys():
- dist = jaro_winkler(membername, name)
- if dist > bestdist:
- bestdist = dist
- bestname = name
- if bestname == None:
- raise RequestError("Unknown idol name: " + membername)
- membername = bestname
- except ImportError:
- raise RequestError("Unknown idol name: " + membername + " (Misspelled? Try installing the Levenshtein module to allow this script to correct small typos. pip3 install python-Levenshtein)")
- member_id = namemap[membername]
- try:
- cards = get_cards(member_id, number, attribute, role)
- except CardLookupError:
- raise CardLookupError("No matching cards for this request: " + " ".join(request))
- if len(cards) == 1:
- if sys.stdin.isatty() and not debug_in_terminal:
- print(card_summary(cards[0]))
- print(" " + card_url(cards[0]))
- else:
- webbrowser.open(card_url(cards[0]))
- else:
- if sys.stdin.isatty() and not debug_in_terminal:
- print("Multiple cards were found:")
- print()
- for card in cards:
- print(card_summary(card))
- print(" " + card_url(card))
- else:
- with open(TMPFILE, "w") as tfile:
- tfile.write("<html>==========<br>MULTIPLE RESULTS<br>==========<br><br>")
- for card in cards:
- tfile.write("<a href='" + card_url(card) + "'>" + card_summary(card) + "</a><br>")
- tfile.write("</html>")
- webbrowser.open("file://" + TMPFILE)
- # Actual card lookup
- def get_cards(member_id, number, attribute, role):
- global CACHEDIR
- force_cache_refresh = False
- while True:
- # get data from cache or API
- cfilename = os.path.join(CACHEDIR, str(member_id))
- if not os.path.isfile(cfilename) or force_cache_refresh:
- with urllib.request.urlopen("https://allstars.kirara.ca/api/private/cards/member/" + str(member_id) + ".json") as url:
- with open(cfilename, "w") as jfile:
- jfile.write(url.read().decode())
- cards = []
- with open(cfilename, "r") as jfile:
- data = json.loads(jfile.read())
- if number != None: cards = get_card_by_number(data, number)
- else: cards = get_cards_by_attribute_and_role(data, attribute, role)
- if len(cards) == 0:
- if not force_cache_refresh:
- # No matching card - reload cache and try again to see whether it's a new card
- force_cache_refresh = True
- continue
- else:
- # No matching card, already tried reloading cache - this card does not exist
- raise CardLookupError()
- return cards
- # If run directly instead of being imported as a module, take command line arguments as input
- if __name__ == '__main__':
- download_all_caches = False
- if not os.path.isdir(CACHEDIR):
- # first launch! - download all member lists for initial caching
- # but only after getting the requested card... response time is important
- os.makedirs(CACHEDIR)
- download_all_caches = True
- try:
- do_lookup(sys.argv[1:])
- except (RequestError,CardLookupError) as e:
- if sys.stdin.isatty() and not debug_in_terminal:
- print("ERROR:", str(e), file=sys.stderr)
- else:
- with open(TMPFILE, "w") as tfile:
- tfile.write("==========\nERROR\n==========\n\n")
- tfile.write(str(e))
- webbrowser.open("file://" + TMPFILE)
- if download_all_caches == True:
- for member_id in member_ids:
- cfilename = os.path.join(CACHEDIR, str(member_id))
- if not os.path.isfile(cfilename):
- with urllib.request.urlopen("https://allstars.kirara.ca/api/private/cards/member/" + str(member_id) + ".json") as url:
- with open(cfilename, "w") as jfile:
- jfile.write(url.read().decode())
Version History
- v7 - Added Attribute/Type lookup, also a bunch of general “make it nicer” — Suyooo 2021/02/15 16:11 UTC
- v6 - Fix cache not being filled on first run if lookup fails — Suyooo 2020/12/05 14:48 UTC
- v5 - Output in terminal if running in one — Suyooo 2020/12/04 23:13 UTC
- v4 - Caches of Card lists from API do not expire now — Suyooo 2020/12/04 22:51 UTC
- v3 - Added caching of API response — Suyooo 2020/10/08 11:12 UTC
- v2 - Remove unused function — Suyooo 2020/10/07 12:28 UTC
- v1 - Initial Release — Suyooo 2020/10/07 11:30 UTC
Possible Future Ideas
- Make it even more resilent to typos: Typo fixing is only active on the member name - could be applied to full attribute name as well, and for most roles one matching character is enough as well
- Make the web page for errors/multiple results nicer by some (very little) styling
- Option for translated names if available?
Contributors to this page: