Sfoglia il codice sorgente

[beers] Allow scrobbling from URLS

Colin Powell 6 mesi fa
parent
commit
7671644e87

+ 40 - 4
vrobbler/apps/beers/models.py

@@ -8,6 +8,10 @@ from imagekit.models import ImageSpecField
 from imagekit.processors import ResizeToFit
 from scrobbles.dataclasses import BeerLogData
 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}
 
@@ -17,6 +21,9 @@ class BeerStyle(TimeStampedModel):
     name = models.CharField(max_length=255)
     description = models.TextField(**BNULL)
 
+    def __str__(self):
+        return self.name
+
 
 class BeerProducer(TimeStampedModel):
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
@@ -29,6 +36,9 @@ class BeerProducer(TimeStampedModel):
     def find_or_create(cls, title: str) -> "BeerProducer":
         return cls.objects.filter(title=title).first()
 
+    def __str__(self):
+        return self.name
+
 
 class Beer(ScrobblableMixin):
     description = models.TextField(**BNULL)
@@ -60,7 +70,8 @@ class Beer(ScrobblableMixin):
     def get_absolute_url(self) -> str:
         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.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:
         url = ""
-        if self.beeradvocate_image:
-            url = self.beeradvocate_image.url
+        if self.untappd_image:
+            url = self.untappd_image.url
         return url
 
     @property
@@ -84,7 +95,32 @@ class Beer(ScrobblableMixin):
 
     @classmethod
     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):
         Scrobble = apps.get_model("scrobbles", "Scrobble")

+ 136 - 0
vrobbler/apps/beers/untappd.py

@@ -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

+ 1 - 1
vrobbler/apps/webpages/models.py

@@ -221,7 +221,7 @@ class WebPage(ScrobblableMixin):
             self.save()
 
     @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
         the url, creating if if doesn't exist yet.
 

+ 1 - 1
vrobbler/templates/beers/beer_detail.html

@@ -34,7 +34,7 @@
         <hr />
         {% endif %}
         <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>
     </div>
 </div>