Add boardgames as scrobblable

This commit is contained in:
2023-04-17 18:29:39 -04:00
parent 4654902adc
commit 0217c96faf
15 changed files with 517 additions and 0 deletions

View File

@ -0,0 +1,30 @@
from django.contrib import admin
from boardgames.models import BoardGame, BoardGamePublisher
from scrobbles.admin import ScrobbleInline
@admin.register(BoardGamePublisher)
class BoardGamePublisherAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = (
"name",
"uuid",
)
ordering = ("-created",)
@admin.register(BoardGame)
class GameAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = (
"bggeek_id",
"title",
"published_date",
)
search_fields = ("title",)
ordering = ("-created",)
inlines = [
ScrobbleInline,
]

View File

@ -0,0 +1,60 @@
from typing import Optional
import requests
from bs4 import BeautifulSoup
SEARCH_ID_URL = "https://boardgamegeek.com/xmlapi/search?search={query}"
GAME_ID_URL = "https://boardgamegeek.com/xmlapi/boardgame/{id}"
def take_first(thing: Optional[list]) -> str:
first = ""
try:
first = thing[0]
except IndexError:
pass
if first:
first = first.get_text()
return first
def get_id_from_bgg(title):
soup = ""
headers = {"User-Agent": "Vrobbler 0.11.12"}
url = SEARCH_ID_URL.format(query=title)
r = requests.get(url, headers=headers)
if r.status_code == 200:
soup = BeautifulSoup(r.text, "xml")
return soup
def get_game_by_id_from_bgg(game_id):
soup = None
game_dict = {}
headers = {"User-Agent": "Vrobbler 0.11.12"}
url = GAME_ID_URL.format(id=game_id)
r = requests.get(url, headers=headers)
if r.status_code == 200:
soup = BeautifulSoup(r.text, "xml")
if soup:
seconds_to_play = None
minutes = take_first(soup.findAll("playingtime"))
if minutes:
seconds_to_play = int(minutes) * 60
game_dict = {
"title": take_first(soup.findAll("name", primary="true")),
"description": take_first(soup.findAll("description")),
"year_published": take_first(soup.findAll("yearpublished")),
"publisher_name": take_first(soup.findAll("boardgamepublisher")),
"cover_url": take_first(soup.findAll("image")),
"min_players": take_first(soup.findAll("minplayers")),
"max_players": take_first(soup.findAll("maxplayers")),
"recommended_age": take_first(soup.findAll("age")),
"run_time_seconds": seconds_to_play,
}
return game_dict

View File

@ -0,0 +1,168 @@
# Generated by Django 4.1.7 on 2023-04-17 22:11
from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
import taggit.managers
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
("scrobbles", "0038_alter_objectwithgenres_tag"),
]
operations = [
migrations.CreateModel(
name="BoardGamePublisher",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created"
),
),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified"
),
),
("name", models.CharField(max_length=255)),
(
"uuid",
models.UUIDField(
blank=True,
default=uuid.uuid4,
editable=False,
null=True,
),
),
(
"logo",
models.ImageField(
blank=True,
null=True,
upload_to="games/platform-logos/",
),
),
("igdb_id", models.IntegerField(blank=True, null=True)),
],
options={
"get_latest_by": "modified",
"abstract": False,
},
),
migrations.CreateModel(
name="BoardGame",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created"
),
),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified"
),
),
(
"run_time_seconds",
models.IntegerField(blank=True, null=True),
),
(
"run_time_ticks",
models.PositiveBigIntegerField(blank=True, null=True),
),
("title", models.CharField(max_length=255)),
(
"uuid",
models.UUIDField(
blank=True,
default=uuid.uuid4,
editable=False,
null=True,
),
),
(
"cover",
models.ImageField(
blank=True, null=True, upload_to="boardgames/covers/"
),
),
(
"layout_image",
models.ImageField(
blank=True, null=True, upload_to="boardgames/layouts/"
),
),
("summary", models.TextField(blank=True, null=True)),
("rating", models.FloatField(blank=True, null=True)),
(
"max_players",
models.PositiveSmallIntegerField(blank=True, null=True),
),
(
"min_players",
models.PositiveSmallIntegerField(blank=True, null=True),
),
("published_date", models.DateField(blank=True, null=True)),
(
"recommened_age",
models.PositiveSmallIntegerField(blank=True, null=True),
),
(
"seconds_to_play",
models.IntegerField(blank=True, null=True),
),
(
"bggeek_id",
models.CharField(blank=True, max_length=255, null=True),
),
(
"genre",
taggit.managers.TaggableManager(
help_text="A comma-separated list of tags.",
through="scrobbles.ObjectWithGenres",
to="scrobbles.Genre",
verbose_name="Tags",
),
),
(
"publisher",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to="boardgames.boardgamepublisher",
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-04-17 22:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("boardgames", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="boardgame",
name="description",
field=models.TextField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-04-17 22:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("boardgames", "0002_boardgame_description"),
]
operations = [
migrations.RenameField(
model_name="boardgame",
old_name="recommened_age",
new_name="recommended_age",
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2023-04-17 22:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("boardgames", "0003_rename_recommened_age_boardgame_recommended_age"),
]
operations = [
migrations.RemoveField(
model_name="boardgame",
name="seconds_to_play",
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2023-04-17 22:29
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("boardgames", "0004_remove_boardgame_seconds_to_play"),
]
operations = [
migrations.RemoveField(
model_name="boardgame",
name="summary",
),
]

View File

@ -0,0 +1,128 @@
import logging
from datetime import datetime
from typing import Optional
from uuid import uuid4
import requests
from boardgames.bgg import get_game_by_id_from_bgg
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
from scrobbles.mixins import ScrobblableMixin
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
class BoardGamePublisher(TimeStampedModel):
name = models.CharField(max_length=255)
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
logo = models.ImageField(upload_to="games/platform-logos/", **BNULL)
igdb_id = models.IntegerField(**BNULL)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse(
"boardgames:publisher_detail", kwargs={"slug": self.uuid}
)
class BoardGame(ScrobblableMixin):
COMPLETION_PERCENT = getattr(
settings, "BOARD_GAME_COMPLETION_PERCENT", 100
)
FIELDS_FROM_BGGEEK = [
"igdb_id",
"alternative_name",
"rating",
"rating_count",
"release_date",
"cover",
"screenshot",
]
title = models.CharField(max_length=255)
publisher = models.ForeignKey(
BoardGamePublisher, **BNULL, on_delete=models.DO_NOTHING
)
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
description = models.TextField(**BNULL)
cover = models.ImageField(upload_to="boardgames/covers/", **BNULL)
layout_image = models.ImageField(upload_to="boardgames/layouts/", **BNULL)
rating = models.FloatField(**BNULL)
max_players = models.PositiveSmallIntegerField(**BNULL)
min_players = models.PositiveSmallIntegerField(**BNULL)
published_date = models.DateField(**BNULL)
recommended_age = models.PositiveSmallIntegerField(**BNULL)
bggeek_id = models.CharField(max_length=255, **BNULL)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse(
"boardgames:boardgame_detail", kwargs={"slug": self.uuid}
)
def bggeek_link(self):
link = ""
if self.bggeek_id:
link = f"https://boardgamegeek.com/boardgame/{self.bggeek_id}"
return link
def fix_metadata(self, data: dict = {}, force_update=False) -> None:
if not self.bggeek_id or force_update:
if not data:
data = get_game_by_id_from_bgg(self.bggeek_id)
cover_url = data.pop("cover_url")
year = data.pop("year_published")
publisher_name = data.pop("publisher_name")
if year:
data["published_date"] = datetime(int(year), 1, 1)
# Fun trick for updating all fields at once
BoardGame.objects.filter(pk=self.id).update(**data)
self.refresh_from_db()
# Add publishers
(
self.publisher,
_created,
) = BoardGamePublisher.objects.get_or_create(name=publisher_name)
self.save()
# Go get cover image if the URL is present
if cover_url and not self.cover:
headers = {"User-Agent": "Vrobbler 0.11.12"}
r = requests.get(cover_url, headers=headers)
logger.debug(r.status_code)
if r.status_code == 200:
fname = f"{self.title}_cover_{self.uuid}.jpg"
self.cover.save(fname, ContentFile(r.content), save=True)
logger.debug("Loaded cover image from BGGeek")
@classmethod
def find_or_create(cls, lookup_id: str) -> Optional["BoardGame"]:
"""Given a Lookup ID (either BGG or BGA ID), return a board game object"""
data = {}
boardgame = None
data = get_game_by_id_from_bgg(lookup_id)
if data:
boardgame, created = cls.objects.get_or_create(
title=data["title"], bggeek_id=lookup_id
)
if created:
boardgame.fix_metadata(data=data)
return boardgame

View File

View File

@ -0,0 +1,25 @@
# Generated by Django 4.1.7 on 2023-04-17 22:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("scrobbles", "0037_scrobble_media_type"),
]
operations = [
migrations.AlterField(
model_name="objectwithgenres",
name="tag",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s_items",
to="scrobbles.genre",
),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.1.7 on 2023-04-17 22:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("boardgames", "0001_initial"),
("scrobbles", "0038_alter_objectwithgenres_tag"),
]
operations = [
migrations.AddField(
model_name="scrobble",
name="board_game",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to="boardgames.boardgame",
),
),
]

View File

@ -27,6 +27,7 @@ class ObjectWithGenres(GenericTaggedItemBase):
Genre,
on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s_items",
**BNULL,
)

View File

@ -4,6 +4,7 @@ import logging
from typing import Optional
from uuid import uuid4
from boardgames.models import BoardGame
from books.models import Book
from django.conf import settings
from django.contrib.auth import get_user_model
@ -434,6 +435,9 @@ class Scrobble(TimeStampedModel):
video_game = models.ForeignKey(
VideoGame, on_delete=models.DO_NOTHING, **BNULL
)
board_game = models.ForeignKey(
BoardGame, on_delete=models.DO_NOTHING, **BNULL
)
media_type = models.CharField(
max_length=14, choices=MediaType.choices, default=MediaType.VIDEO
)
@ -583,6 +587,8 @@ class Scrobble(TimeStampedModel):
media_obj = self.book
if self.video_game:
media_obj = self.video_game
if self.board_game:
media_obj = self.board_game
return media_obj
def __str__(self):
@ -614,6 +620,9 @@ class Scrobble(TimeStampedModel):
if media_class == "VideoGame":
media_query = models.Q(video_game=media)
scrobble_data["video_game_id"] = media.id
if media_class == "BoardGame":
media_query = models.Q(board_game=media)
scrobble_data["board_game_id"] = media.id
scrobble = (
cls.objects.filter(

View File

@ -112,6 +112,7 @@ INSTALLED_APPS = [
"podcasts",
"sports",
"books",
"boardgames",
"videogames",
"mathfilters",
"rest_framework",