Add board game urls and views
This commit is contained in:
@ -3,7 +3,9 @@ from typing import Optional
|
|||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
SEARCH_ID_URL = "https://boardgamegeek.com/xmlapi/search?search={query}"
|
SEARCH_ID_URL = (
|
||||||
|
"https://boardgamegeek.com/xmlapi/search?search={query}&exact=1"
|
||||||
|
)
|
||||||
GAME_ID_URL = "https://boardgamegeek.com/xmlapi/boardgame/{id}"
|
GAME_ID_URL = "https://boardgamegeek.com/xmlapi/boardgame/{id}"
|
||||||
|
|
||||||
|
|
||||||
@ -20,21 +22,41 @@ def take_first(thing: Optional[list]) -> str:
|
|||||||
return first
|
return first
|
||||||
|
|
||||||
|
|
||||||
def get_id_from_bgg(title):
|
def lookup_boardgame_id_from_bgg(title: str) -> Optional[int]:
|
||||||
soup = ""
|
soup = None
|
||||||
headers = {"User-Agent": "Vrobbler 0.11.12"}
|
headers = {"User-Agent": "Vrobbler 0.11.12"}
|
||||||
|
game_id = None
|
||||||
url = SEARCH_ID_URL.format(query=title)
|
url = SEARCH_ID_URL.format(query=title)
|
||||||
r = requests.get(url, headers=headers)
|
r = requests.get(url, headers=headers)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
soup = BeautifulSoup(r.text, "xml")
|
soup = BeautifulSoup(r.text, "xml")
|
||||||
return soup
|
|
||||||
|
if soup:
|
||||||
|
result = soup.findAll("boardgame")
|
||||||
|
if not result:
|
||||||
|
return game_id
|
||||||
|
|
||||||
|
game_id = result[0].get("objectid", None)
|
||||||
|
|
||||||
|
return game_id
|
||||||
|
|
||||||
|
|
||||||
def get_game_by_id_from_bgg(game_id):
|
def lookup_boardgame_from_bgg(lookup_id: str) -> dict:
|
||||||
soup = None
|
soup = None
|
||||||
game_dict = {}
|
game_dict = {}
|
||||||
headers = {"User-Agent": "Vrobbler 0.11.12"}
|
headers = {"User-Agent": "Vrobbler 0.11.12"}
|
||||||
url = GAME_ID_URL.format(id=game_id)
|
|
||||||
|
title = ""
|
||||||
|
bgg_id = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
bgg_id = int(lookup_id)
|
||||||
|
except ValueError:
|
||||||
|
title = lookup_id
|
||||||
|
|
||||||
|
if not bgg_id:
|
||||||
|
bgg_id = lookup_boardgame_id_from_bgg(title)
|
||||||
|
url = GAME_ID_URL.format(id=bgg_id)
|
||||||
r = requests.get(url, headers=headers)
|
r = requests.get(url, headers=headers)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
soup = BeautifulSoup(r.text, "xml")
|
soup = BeautifulSoup(r.text, "xml")
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from boardgames.bgg import get_game_by_id_from_bgg
|
from boardgames.bgg import lookup_boardgame_from_bgg
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -69,6 +69,12 @@ class BoardGame(ScrobblableMixin):
|
|||||||
"boardgames:boardgame_detail", kwargs={"slug": self.uuid}
|
"boardgames:boardgame_detail", kwargs={"slug": self.uuid}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def primary_image_url(self) -> str:
|
||||||
|
url = ""
|
||||||
|
if self.cover:
|
||||||
|
url = self.cover.url
|
||||||
|
return url
|
||||||
|
|
||||||
def bggeek_link(self):
|
def bggeek_link(self):
|
||||||
link = ""
|
link = ""
|
||||||
if self.bggeek_id:
|
if self.bggeek_id:
|
||||||
@ -111,12 +117,14 @@ class BoardGame(ScrobblableMixin):
|
|||||||
logger.debug("Loaded cover image from BGGeek")
|
logger.debug("Loaded cover image from BGGeek")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls, lookup_id: str) -> Optional["BoardGame"]:
|
def find_or_create(
|
||||||
|
cls, lookup_id: str, data: Optional[dict] = {}
|
||||||
|
) -> Optional["BoardGame"]:
|
||||||
"""Given a Lookup ID (either BGG or BGA ID), return a board game object"""
|
"""Given a Lookup ID (either BGG or BGA ID), return a board game object"""
|
||||||
data = {}
|
|
||||||
boardgame = None
|
boardgame = None
|
||||||
|
|
||||||
data = get_game_by_id_from_bgg(lookup_id)
|
if not data:
|
||||||
|
data = lookup_boardgame_from_bgg(lookup_id)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
boardgame, created = cls.objects.get_or_create(
|
boardgame, created = cls.objects.get_or_create(
|
||||||
|
|||||||
21
vrobbler/apps/boardgames/urls.py
Normal file
21
vrobbler/apps/boardgames/urls.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from boardgames import views
|
||||||
|
|
||||||
|
app_name = "boardgames"
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"board-game/", views.BoardGameListView.as_view(), name="boardgame_list"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"board-game/<slug:slug>/",
|
||||||
|
views.BoardGameDetailView.as_view(),
|
||||||
|
name="boardgame_detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"board-game-publisher/<slug:slug>/",
|
||||||
|
views.BoardGamePublisherDetailView.as_view(),
|
||||||
|
name="publisher_detail",
|
||||||
|
),
|
||||||
|
]
|
||||||
17
vrobbler/apps/boardgames/views.py
Normal file
17
vrobbler/apps/boardgames/views.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.views import generic
|
||||||
|
from boardgames.models import BoardGame, BoardGamePublisher
|
||||||
|
|
||||||
|
|
||||||
|
class BoardGameListView(generic.ListView):
|
||||||
|
model = BoardGame
|
||||||
|
paginate_by = 20
|
||||||
|
|
||||||
|
|
||||||
|
class BoardGameDetailView(generic.DetailView):
|
||||||
|
model = BoardGame
|
||||||
|
slug_field = "uuid"
|
||||||
|
|
||||||
|
|
||||||
|
class BoardGamePublisherDetailView(generic.DetailView):
|
||||||
|
model = BoardGamePublisher
|
||||||
|
slug_field = "uuid"
|
||||||
82
vrobbler/templates/boardgames/boardgame_detail.html
Normal file
82
vrobbler/templates/boardgames/boardgame_detail.html
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{% extends "base_list.html" %}
|
||||||
|
{% load mathfilters %}
|
||||||
|
{% load static %}
|
||||||
|
{% load naturalduration %}
|
||||||
|
|
||||||
|
{% block title %}{{object.title}}{% endblock %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<style>
|
||||||
|
.cover img {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
float: left;
|
||||||
|
width: 252px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
float: left;
|
||||||
|
width: 600px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block lists %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% if object.cover %}
|
||||||
|
<div class="cover"><img src="{{object.cover.url}}" /></div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="summary">
|
||||||
|
{% if object.description%}
|
||||||
|
<p>{{object.description|safe|linebreaks|truncatewords:160}}</p>
|
||||||
|
<hr />
|
||||||
|
{% endif %}
|
||||||
|
<p style="float:right;">
|
||||||
|
<a href="{{object.bggeek_link}}"><img src="{% static "images/bgg-logo.png" %}" width=35></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<p>{{object.scrobble_set.count}} scrobbles</p>
|
||||||
|
<p>{{object.scrobble_set.last.long_play_seconds|natural_duration}}{% if object.scrobble_set.last.long_play_complete %} and completed{% else %} spent playing{% endif %}</p>
|
||||||
|
<p>
|
||||||
|
{% if object.scrobble_set.last.long_play_complete == True %}
|
||||||
|
<a href="">Play again</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{object.get_start_url}}">Resume playing</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<h3>Last scrobbles</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">Completed</th>
|
||||||
|
<th scope="col">Duration</th>
|
||||||
|
<th scope="col">Platforms</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
|
||||||
|
<tr>
|
||||||
|
<td>{{scrobble.timestamp}}</td>
|
||||||
|
<td>{% if scrobble.long_play_complete == True %}Yes{% endif %}</td>
|
||||||
|
<td>{% if scrobble.in_progress %}Now playing{% else %}{{scrobble.playback_position_seconds|natural_duration}}{% endif %}</td>
|
||||||
|
<td>{% for platform in scrobble.video_game.platforms.all %}<a href="{{platform.get_absolute_url}}">{{platform}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
82
vrobbler/templates/boardgames/boardgame_list.html
Normal file
82
vrobbler/templates/boardgames/boardgame_list.html
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{% extends "base_list.html" %}
|
||||||
|
{% load urlreplace %}
|
||||||
|
|
||||||
|
{% block title %}Albums{% endblock %}
|
||||||
|
|
||||||
|
{% block lists %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<p class="view">
|
||||||
|
<span class="view-links">
|
||||||
|
{% if view == 'grid' %}
|
||||||
|
View as <a href="?{% urlreplace view='list' %}">List</a>
|
||||||
|
{% else %}
|
||||||
|
View as <a href="?{% urlreplace view='grid' %}">Grid</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="pagination">
|
||||||
|
<span class="page-links">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
|
||||||
|
{% endif %}
|
||||||
|
<span class="page-current">
|
||||||
|
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
{% if view == 'grid' %}
|
||||||
|
<div>
|
||||||
|
{% for game in object_list %}
|
||||||
|
{% if game.cover %}
|
||||||
|
<dl style="width: 130px; float: left; margin-right:10px;">
|
||||||
|
<dd><img src="{{game.cover.url}}" width=120 height=120 /></dd>
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Scrobbles</th>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Publisher</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for game in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{game.scrobbles.count}}</td>
|
||||||
|
<td><a href="{{game.get_absolute_url}}">{{game}}</a></td>
|
||||||
|
<td><a href="{{game.publisher.get_absolute_url}}">{{game.publisher}}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="pagination" style="margin-bottom:50px;">
|
||||||
|
<span class="page-links">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
|
||||||
|
{% endif %}
|
||||||
|
<span class="page-current">
|
||||||
|
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -9,6 +9,7 @@ from vrobbler.apps.books import urls as book_urls
|
|||||||
from vrobbler.apps.sports import urls as sports_urls
|
from vrobbler.apps.sports import urls as sports_urls
|
||||||
from vrobbler.apps.podcasts import urls as podcast_urls
|
from vrobbler.apps.podcasts import urls as podcast_urls
|
||||||
from vrobbler.apps.videogames import urls as videogame_urls
|
from vrobbler.apps.videogames import urls as videogame_urls
|
||||||
|
from vrobbler.apps.boardgames import urls as boardgame_urls
|
||||||
from vrobbler.apps.music.api.views import (
|
from vrobbler.apps.music.api.views import (
|
||||||
AlbumViewSet,
|
AlbumViewSet,
|
||||||
ArtistViewSet,
|
ArtistViewSet,
|
||||||
@ -63,6 +64,7 @@ urlpatterns = [
|
|||||||
path("", include(book_urls, namespace="books")),
|
path("", include(book_urls, namespace="books")),
|
||||||
path("", include(video_urls, namespace="videos")),
|
path("", include(video_urls, namespace="videos")),
|
||||||
path("", include(videogame_urls, namespace="videogames")),
|
path("", include(videogame_urls, namespace="videogames")),
|
||||||
|
path("", include(boardgame_urls, namespace="boardgames")),
|
||||||
path("", include(sports_urls, namespace="sports")),
|
path("", include(sports_urls, namespace="sports")),
|
||||||
path("", include(podcast_urls, namespace="podcasts")),
|
path("", include(podcast_urls, namespace="podcasts")),
|
||||||
path("", include(scrobble_urls, namespace="scrobbles")),
|
path("", include(scrobble_urls, namespace="scrobbles")),
|
||||||
|
|||||||
Reference in New Issue
Block a user