[beers] Allow scrobbling from URLS
This commit is contained in:
@ -8,6 +8,10 @@ from imagekit.models import ImageSpecField
|
|||||||
from imagekit.processors import ResizeToFit
|
from imagekit.processors import ResizeToFit
|
||||||
from scrobbles.dataclasses import BeerLogData
|
from scrobbles.dataclasses import BeerLogData
|
||||||
from scrobbles.mixins import ScrobblableMixin
|
from scrobbles.mixins import ScrobblableMixin
|
||||||
|
from vrobbler.apps.beers.untappd import (
|
||||||
|
get_beer_from_untappd_id,
|
||||||
|
get_rating_from_soup,
|
||||||
|
)
|
||||||
|
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
|
|
||||||
@ -17,6 +21,9 @@ class BeerStyle(TimeStampedModel):
|
|||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(**BNULL)
|
description = models.TextField(**BNULL)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class BeerProducer(TimeStampedModel):
|
class BeerProducer(TimeStampedModel):
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
@ -29,6 +36,9 @@ class BeerProducer(TimeStampedModel):
|
|||||||
def find_or_create(cls, title: str) -> "BeerProducer":
|
def find_or_create(cls, title: str) -> "BeerProducer":
|
||||||
return cls.objects.filter(title=title).first()
|
return cls.objects.filter(title=title).first()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Beer(ScrobblableMixin):
|
class Beer(ScrobblableMixin):
|
||||||
description = models.TextField(**BNULL)
|
description = models.TextField(**BNULL)
|
||||||
@ -60,7 +70,8 @@ class Beer(ScrobblableMixin):
|
|||||||
def get_absolute_url(self) -> str:
|
def get_absolute_url(self) -> str:
|
||||||
return reverse("beers:beer_detail", kwargs={"slug": self.uuid})
|
return reverse("beers:beer_detail", kwargs={"slug": self.uuid})
|
||||||
|
|
||||||
def beeradvocate_link(self):
|
def beeradvocate_link(self) -> str:
|
||||||
|
link = ""
|
||||||
if self.producer and self.beeradvocate_id:
|
if self.producer and self.beeradvocate_id:
|
||||||
if self.beeradvocate_id:
|
if self.beeradvocate_id:
|
||||||
link = f"https://www.beeradvocate.com/beer/profile/{self.producer.beeradvocate_id}/{self.beeradvocate_id}/"
|
link = f"https://www.beeradvocate.com/beer/profile/{self.producer.beeradvocate_id}/{self.beeradvocate_id}/"
|
||||||
@ -74,8 +85,8 @@ class Beer(ScrobblableMixin):
|
|||||||
|
|
||||||
def primary_image_url(self) -> str:
|
def primary_image_url(self) -> str:
|
||||||
url = ""
|
url = ""
|
||||||
if self.beeradvocate_image:
|
if self.untappd_image:
|
||||||
url = self.beeradvocate_image.url
|
url = self.untappd_image.url
|
||||||
return url
|
return url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -84,7 +95,32 @@ class Beer(ScrobblableMixin):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls, untappd_id: str) -> "Beer":
|
def find_or_create(cls, untappd_id: str) -> "Beer":
|
||||||
return cls.objects.filter(untappd_id=untappd_id).first()
|
beer = cls.objects.filter(untappd_id=untappd_id).first()
|
||||||
|
|
||||||
|
if not beer:
|
||||||
|
beer_dict = get_beer_from_untappd_id(untappd_id)
|
||||||
|
producer_dict = {}
|
||||||
|
style_ids = []
|
||||||
|
for key in list(beer_dict.keys()):
|
||||||
|
if "producer__" in key:
|
||||||
|
pkey = key.replace("producer__", "")
|
||||||
|
producer_dict[pkey] = beer_dict.pop(key)
|
||||||
|
if "styles" in key:
|
||||||
|
for style in beer_dict.pop("styles"):
|
||||||
|
style_inst, created = BeerStyle.objects.get_or_create(
|
||||||
|
name=style
|
||||||
|
)
|
||||||
|
style_ids.append(style_inst.id)
|
||||||
|
|
||||||
|
producer, _created = BeerProducer.objects.get_or_create(
|
||||||
|
**producer_dict
|
||||||
|
)
|
||||||
|
beer_dict["producer_id"] = producer.id
|
||||||
|
beer = Beer.objects.create(**beer_dict)
|
||||||
|
for style_id in style_ids:
|
||||||
|
beer.styles.add(style_id)
|
||||||
|
|
||||||
|
return beer
|
||||||
|
|
||||||
def scrobbles(self, user_id):
|
def scrobbles(self, user_id):
|
||||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||||
|
|||||||
136
vrobbler/apps/beers/untappd.py
Normal file
136
vrobbler/apps/beers/untappd.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import urllib
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UNTAPPD_URL = "https://untappd.com/beer/{id}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_first(key: str, result: dict) -> str:
|
||||||
|
obj = ""
|
||||||
|
if obj_list := result.get(key):
|
||||||
|
obj = obj_list[0]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def get_title_from_soup(soup) -> str:
|
||||||
|
title = ""
|
||||||
|
try:
|
||||||
|
title = soup.find("h1").get_text()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
|
def get_description_from_soup(soup) -> str:
|
||||||
|
desc = ""
|
||||||
|
try:
|
||||||
|
desc = soup.find(class_="beer-description-read-less").get_text()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return desc
|
||||||
|
|
||||||
|
|
||||||
|
def get_styles_from_soup(soup) -> list[str]:
|
||||||
|
styles = []
|
||||||
|
try:
|
||||||
|
styles = soup.find("p", class_="style").get_text().split(" - ")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return styles
|
||||||
|
|
||||||
|
|
||||||
|
def get_abv_from_soup(soup) -> Optional[float]:
|
||||||
|
abv = None
|
||||||
|
try:
|
||||||
|
abv = soup.find(class_="abv").get_text()
|
||||||
|
if abv:
|
||||||
|
abv = float(abv.strip("\n").strip("% ABV").strip())
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return abv
|
||||||
|
|
||||||
|
|
||||||
|
def get_ibu_from_soup(soup) -> Optional[int]:
|
||||||
|
ibu = None
|
||||||
|
try:
|
||||||
|
ibu = soup.find(class_="ibu").get_text()
|
||||||
|
if ibu:
|
||||||
|
ibu = int(ibu.strip("\n").strip(" IBU").strip())
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
ibu = None
|
||||||
|
return ibu
|
||||||
|
|
||||||
|
|
||||||
|
def get_rating_from_soup(soup) -> str:
|
||||||
|
rating = ""
|
||||||
|
try:
|
||||||
|
rating = soup.find(class_="num").get_text().strip("(").strip(")")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return rating
|
||||||
|
|
||||||
|
|
||||||
|
def get_producer_id_from_soup(soup) -> str:
|
||||||
|
id = ""
|
||||||
|
try:
|
||||||
|
id = soup.find(class_="brewery").find("a")["href"].strip("/")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return id
|
||||||
|
|
||||||
|
|
||||||
|
def get_producer_name_from_soup(soup) -> str:
|
||||||
|
name = ""
|
||||||
|
try:
|
||||||
|
name = soup.find(class_="brewery").find("a").get_text()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def get_beer_from_untappd_id(untappd_id: str) -> dict:
|
||||||
|
beer_url = UNTAPPD_URL.format(id=untappd_id)
|
||||||
|
headers = {"User-Agent": "Vrobbler 0.11.12"}
|
||||||
|
response = requests.get(beer_url, headers=headers)
|
||||||
|
beer_dict = {"untappd_id": untappd_id}
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.warn(
|
||||||
|
"Bad response from untappd.com", extra={"response": response}
|
||||||
|
)
|
||||||
|
return beer_dict
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, "html.parser")
|
||||||
|
beer_dict["untappd_id"] = untappd_id
|
||||||
|
beer_dict["title"] = get_title_from_soup(soup)
|
||||||
|
beer_dict["description"] = get_description_from_soup(soup)
|
||||||
|
beer_dict["styles"] = get_styles_from_soup(soup)
|
||||||
|
beer_dict["abv"] = get_abv_from_soup(soup)
|
||||||
|
beer_dict["ibu"] = get_ibu_from_soup(soup)
|
||||||
|
beer_dict["untappd_rating"] = get_rating_from_soup(soup)
|
||||||
|
beer_dict["producer__untappd_id"] = get_producer_id_from_soup(soup)
|
||||||
|
beer_dict["producer__name"] = get_producer_name_from_soup(soup)
|
||||||
|
|
||||||
|
return beer_dict
|
||||||
@ -221,7 +221,7 @@ class WebPage(ScrobblableMixin):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls, data_dict: Dict) -> "GeoLocation":
|
def find_or_create(cls, data_dict: Dict) -> "WebPage":
|
||||||
"""Given a data dict from an manual URL scrobble, does the heavy lifting of looking up
|
"""Given a data dict from an manual URL scrobble, does the heavy lifting of looking up
|
||||||
the url, creating if if doesn't exist yet.
|
the url, creating if if doesn't exist yet.
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p style="float:right;">
|
<p style="float:right;">
|
||||||
<a href="{{object.beeradvocate_link}}"><img src="{% static "images/beeradvoate-logo.png" %}" width=35></a>
|
<a href="{{object.untappd_link}}"><img src="{% static "images/untappd-logo.png" %}" width=35></a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user