From d78529efe274a869db0ce78c4ffc8191f07cf505 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sat, 4 Jul 2026 11:41:24 -0400 Subject: [PATCH] [boardgames] Add genres and categories --- PROJECT.org | 6 +- .../commands/cleanup_boardgame_metadata.py | 81 +++++++++++++++++++ vrobbler/apps/boardgames/models.py | 14 +++- vrobbler/apps/boardgames/sources/bgg.py | 1 + vrobbler/apps/boardgames/tests/test_models.py | 1 + .../boardgames/boardgame_detail.html | 14 ++++ 6 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 vrobbler/apps/boardgames/management/commands/cleanup_boardgame_metadata.py diff --git a/PROJECT.org b/PROJECT.org index bb58191..99d908c 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -619,7 +619,6 @@ The Edit log form should have from top to bottom: - Expansion ids (which should a multi-select widget of expansions for this game) - Location (which should be a drop down of BoardGameLocations for this user) -** TODO Board games should have genres extracted from family data :boardgames:metadata: ** TODO Fix bug in fetching expansions for board games :boardgames: :PROPERTIES: :ID: 17995312-e76e-4a50-b591-0eab78cb59ab @@ -643,6 +642,11 @@ The Edit log form should have from top to bottom: boardgamegeek.exceptions.BGGApiError: invalid data for game id: 242117 #+end_src +** TODO Board games should have genres extracted from family data :boardgames:metadata: +:PROPERTIES: +:ID: 7214b270-dccc-4b98-ac58-ff4f76c8cda9 +:END: + ** DONE Exclude some board games from auto-expansion imports :boardgames: :PROPERTIES: :ID: 51ffdf20-e732-4774-b781-c3501d26d46f diff --git a/vrobbler/apps/boardgames/management/commands/cleanup_boardgame_metadata.py b/vrobbler/apps/boardgames/management/commands/cleanup_boardgame_metadata.py new file mode 100644 index 0000000..ec98d3c --- /dev/null +++ b/vrobbler/apps/boardgames/management/commands/cleanup_boardgame_metadata.py @@ -0,0 +1,81 @@ +import logging +import time + +from django.core.management.base import BaseCommand + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Refresh board game metadata from BGG (categories→genres, families→tags)" + + def add_arguments(self, parser): + parser.add_argument( + "--commit", + action="store_true", + help="Persist changes to the database", + ) + parser.add_argument( + "--force", + action="store_true", + help="Update all games even if they already have a published_date", + ) + parser.add_argument( + "--batch-size", + type=int, + default=50, + help="Number of games to process per batch (default: 50)", + ) + parser.add_argument( + "--sleep", + type=float, + default=1.0, + help="Seconds to sleep between API calls (default: 1.0)", + ) + + def handle(self, *args, **options): + from boardgames.models import BoardGame + + commit = options["commit"] + force = options["force"] + batch_size = options["batch_size"] + sleep_secs = options["sleep"] + + qs = BoardGame.objects.exclude(bggeek_id__isnull=True).exclude(bggeek_id="") + total = qs.count() + self.stdout.write(f"Found {total} board games with BGG IDs") + + if not commit: + self.stdout.write( + "Dry run — no API calls will be made. Use --commit to run lookups." + ) + return + + enriched = 0 + skipped = 0 + + for batch_num, offset in enumerate(range(0, total, batch_size)): + batch = qs[offset : offset + batch_size] + for game in batch: + try: + game.fix_metadata(force_update=force) + enriched += 1 + except Exception as e: + self.stdout.write( + self.style.WARNING( + f" [SKIPPED] {game.title} (BGG {game.bggeek_id}): {e}" + ) + ) + skipped += 1 + time.sleep(sleep_secs) + + self.stdout.write( + f" Batch {batch_num + 1}: {offset + len(batch)}/{total} — " + f"enriched: {enriched}, skipped: {skipped}" + ) + + self.stdout.write( + f"\nResults:\n" + f" Games enriched: {enriched}\n" + f" Games skipped: {skipped}" + ) diff --git a/vrobbler/apps/boardgames/models.py b/vrobbler/apps/boardgames/models.py index f0647b2..c3bd442 100644 --- a/vrobbler/apps/boardgames/models.py +++ b/vrobbler/apps/boardgames/models.py @@ -351,8 +351,9 @@ class BoardGame(ScrobblableMixin): data.pop("max_players") # Pop extra BGG metadata that isn't a model field + categories = data.pop("categories", []) + families = data.pop("families", []) data.pop("mechanics", None) - data.pop("categories", None) data.pop("designers", None) data.pop("publishers", None) data.pop("publisher", None) @@ -368,6 +369,11 @@ class BoardGame(ScrobblableMixin): ) = BoardGamePublisher.objects.get_or_create(name=publisher_name) self.save() + for cat in categories: + self.genre.add(cat.strip()) + for fam in families: + self.tags.add(fam.strip()) + # Go get cover image if the URL is present if cover_url and not self.cover: self.save_image_from_url(cover_url) @@ -417,6 +423,7 @@ class BoardGame(ScrobblableMixin): mechanics = bgg_data.pop("mechanics", []) designers = bgg_data.pop("designers", []) categories = bgg_data.pop("categories", []) + families = bgg_data.pop("families", []) publishers = bgg_data.pop("publishers", []) publisher = bgg_data.pop("publisher", []) cover_url = bgg_data.pop("cover_url") or "" @@ -459,6 +466,11 @@ class BoardGame(ScrobblableMixin): publisher, _ = BoardGamePublisher.objects.get_or_create(name=name) game.publishers.add(publisher) + for cat in categories: + game.genre.add(cat.strip()) + for fam in families: + game.tags.add(fam.strip()) + if expansions and not game.skip_expansions: if defer_expansions: from boardgames.tasks import fetch_board_game_expansions diff --git a/vrobbler/apps/boardgames/sources/bgg.py b/vrobbler/apps/boardgames/sources/bgg.py index 68d906b..a9ec453 100644 --- a/vrobbler/apps/boardgames/sources/bgg.py +++ b/vrobbler/apps/boardgames/sources/bgg.py @@ -32,6 +32,7 @@ def lookup_boardgame_from_bgg( ) game_dict["mechanics"] = game.mechanics game_dict["categories"] = game.categories + game_dict["families"] = game.families game_dict["designers"] = game.designers game_dict["publishers"] = game.publishers if game.publishers: diff --git a/vrobbler/apps/boardgames/tests/test_models.py b/vrobbler/apps/boardgames/tests/test_models.py index 7dbb655..a4f3f6c 100644 --- a/vrobbler/apps/boardgames/tests/test_models.py +++ b/vrobbler/apps/boardgames/tests/test_models.py @@ -83,6 +83,7 @@ def _mock_bgg_game(bggeek_id, title, expansions=None): playingtime = 60 mechanics = [] categories = [] + families = [] designers = [] publishers = [] diff --git a/vrobbler/templates/boardgames/boardgame_detail.html b/vrobbler/templates/boardgames/boardgame_detail.html index e86a89e..274f7f1 100644 --- a/vrobbler/templates/boardgames/boardgame_detail.html +++ b/vrobbler/templates/boardgames/boardgame_detail.html @@ -46,6 +46,20 @@

Play again

+ {% if object.genre.all %} +

Genres: + {% for tag in object.genre.all %} + {{ tag.name }} + {% endfor %} +

+ {% endif %} + {% if object.tags.all %} +

Tags: + {% for tag in object.tags.all %} + {{ tag.name }} + {% endfor %} +

+ {% endif %} {% if charts %}