def new_game(): client = BGGClient() not_found = True while not_found: game_search = input_prompt( message=GAME_MESSAGE, validation_function=lambda x: True if x != "" else "Enter something!", ) if game_search.lower() == "r": return games = client.search(game_search) game_names = [game.name for game in games] if not game_names: print("No games found") continue game_question_list = game_names + [GAME_CANCEL_OPTION] answer = list_prompt(message=GAME_CHOICE_MESSAGE, items=game_question_list) if answer == GAME_CANCEL_OPTION: continue if confirmation_prompt( message=f"Are you sure you would like to add {answer}?"): not_found = False add_game(games[game_names.index(answer)])
def search_boardgame(boardgame, game_type=None): """ Uses the boardgamemeek XML API to search for games Currently supports boardgames and TT RPGS """ if game_type is None: search_type = None game_type = 'boardgame' elif game_type == 'RPG': game_type = 'rpgitem' search_type = [BGGRestrictSearchResultsTo.RPG] bgg = BGGClient() res = bgg.search(boardgame, search_type=search_type) # this is terrible if len(res) == 0: return "I couldn't find any game similar to the title %s" % boardgame print([x.data() for x in res[0:10]]) games = [x for x in res if boardgame.lower() == x.name.lower()] resp = "" if len(games) == 0: resp += ("Couldn't find game {} exactly, but I found {} which " + "looks close\n").format(boardgame, len(res)) games = [x for x in res if boardgame.lower() in x.name.lower()] if len(games) == 0: resp += "Well, not exactly close, " resp += "but here's the oldest one on the list\n" games = res g = sorted(games, key=lambda x: x.year)[0] url_format = "https://www.boardgamegeek.com/{game_tp}/{id}/{name}".format( game_tp=game_type, id=g.id, name=g.name.lower().replace(' ', '-')) return resp + url_format
def get(self, request): form = SearchForm(request.GET) if form.is_valid(): bgg = BGGClient() query = form.cleaned_data["query"] query = bgg.search(query) query = [q.data() for q in query] keys = ['id', 'name', 'type', 'yearpublished'] query_items = [[(key, data[key]) for key in keys] for data in query] return render(request, "results.html", { "query_items": query_items, "keys": keys, }) else: return render(request, "main.html", {"form": form})
class BGGManager(): ''' The BGGManager class is a wrapper around the boardgamegeek2 API. Init with an ids file, a names file, and a details file. The instance can optionally download from boardgamegeek, or loaded from disk.''' def __init__(self, ids_file, names_file, details_file): '''ids_file - name of a file with/for BGG ids names_file - name of a file with/for BGG game names details_file - name of a file with/for BGG game details''' self._ids_file = ids_file self._ids = None self._names_file = names_file self._names = None self._details_file = details_file self._bgg = BGGClient() @staticmethod def grouper(n, iterable, fillvalue=None): '''Turns a flat list into a set of groups of length n. BGGManager uses this to group the downloaded ids into batches for retrieval from boargamegeek's batch API. Example: grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx ''' args = [iter(iterable)] * n return zip_longest(fillvalue=fillvalue, *args) @staticmethod def boardgame_to_dict(bg): ''' Turns the structure returned by boardgamegeek2 into a dict, suitable for uploading to Elasticsearch. TODO: there are some deeper substructures - stats and ranks that would be nice to have as well. They don't bear directly, so I have ignored them. Initially I was trying to use class introspection to implement this but not all of the below are actually attributes of the class. This method is messier but actually works.''' return { "accessory": bg.accessory, "alternative_names": bg.alternative_names, "artists": bg.artists, "bgg_rank": bg.bgg_rank, "boardgame_rank": bg.boardgame_rank, "categories": bg.categories, "comments": bg.comments, "description": bg.description, "designers": bg.designers, "expansion": bg.expansion, "families": bg.families, "id": bg.id, "image": bg.image, "implementations": bg.implementations, "max_players": bg.max_players, "max_playing_time": bg.max_playing_time, "mechanics": bg.mechanics, "min_age": bg.min_age, "min_players": bg.min_players, "min_playing_time": bg.min_playing_time, "name": bg.name, "playing_time": bg.playing_time, "publishers": bg.publishers, "rating_average": bg.rating_average, "rating_average_weight": bg.rating_average_weight, "rating_bayes_average": bg.rating_bayes_average, "rating_median": bg.rating_median, "rating_num_weights": bg.rating_num_weights, "rating_stddev": bg.rating_stddev, "thumbnail": bg.thumbnail, "users_commented": bg.users_commented, "users_owned": bg.users_owned, "users_rated": bg.users_rated, "versions": bg.versions, "videos": bg.videos, "year": bg.year } def _download_and_save_games(self): ''' Use the API to pull games from boardgamegeek. There isn't a good way to search generically, the API requires at least 1 character for the wildcard. So I took the approach of wildcarding each letter of the alphabet. This misses some games, even some top games, but gets most everything. Note, since the API is searching across words, there are duplicate names and ids downloaded. This is handled with a set() structure to hold names and ids. Pickles ids and names that it downloads to the names file and ids file.''' self._ids = set() self._names = set() small_letters = map(chr, range(ord('a'), ord('z') + 1)) for letter in small_letters: things = self._bgg.search('{}*'.format(letter)) for thing in things: self._ids.add(thing.id) self._names.add(thing.name) with open(self._ids_file, 'wb') as ids_file: pickle.dump(self._ids, ids_file) with open(self._names_file, 'wb') as names_file: pickle.dump(self._names, names_file) def _load_saved_games(self): '''Loads the pickled game names and ids''' with open(self._names_file, 'rb') as names_file: self._names = pickle.load(names_file) with open(self._ids_file, 'rb') as ids_file: self._ids = pickle.load(ids_file) def load_game_names_and_ids(self, download=False): ''' Entry point for loading game names and ids. Set download=True to use the Boardgame Geek APIs to download them fresh, or False to load pickled data from a prior run. ''' if download: self._download_and_save_games() else: self._load_saved_games() print('Got {} ids and {} names'.format(len(self._ids), len(self._names))) def _download_and_save_game_details(self): ''' Downloads game details from the BoardgameGeek API. This takes the ids and splits them into chunks of 100 so that it can call the BGG batch API for retrieval. After retrieving the game details, this reformats them as a dict for easier transmission to ES. I made the decision to store the text JSON of the details instead of pickling or storing in some other binary format. This facilitates quickly grepping the source data as well as processing to generate test sets. A bunch of the parameters (e.g. group size) might be better specified on the command line. For simplicity, they're hard-coded here.''' n = 0 chunk_size = 100 with open(self._details_file, 'w') as details_file: for chunk in self.grouper(chunk_size, self._ids, fillvalue=None): games = self._bgg.game_list(game_id_list=list(chunk)) n += chunk_size print('Downloaded {} games.'.format(n)) for g in games: try: d = self.boardgame_to_dict(g) if d: json.dump(d, details_file) details_file.write('\n') except Exception as e: print( 'Exception getting details. Skipping "{}".'.format( g.name)) def _load_saved_game_details(self): ''' Loads the game details from the JSON file where they are stored. ''' self._details = list() with open(self._details_file, 'r') as f_in: for line in f_in: dic = json.loads(line.lstrip().rstrip()) self._details.append(dic) print("Loaded {} game records".format(len(self._details))) def load_game_details(self, download=False): ''' Entry point for downloading or loading the game details. Specify download=True to pull from BGG or download=False to load a previous data set. ''' if download: if not self._ids or not self._names: # TODO: Better exception raise Exception( 'Can\'t download game details without loading names and ids first!' ) self._download_and_save_game_details() else: self._load_saved_game_details() def game_details(self): ''' Iterator over the game details. Details must be loaded first. ''' if not self._details: raise ValueError( 'Trying to send iterate game details, but none loaded') for detail in self._details: yield detail