Repository: alltheplaces/alltheplaces

Issue:
New storefinder: Rio SEO
> Seems to be a common self-hosted store finder (new, not sure what product/name) shared with https://github.com/alltheplaces/alltheplaces/issues/810, https://github.com/alltheplaces/alltheplaces/issues/966, https://github.com/alltheplaces/alltheplaces/issues/1005, https://github.com/alltheplaces/alltheplaces/issues/1047, hallmark and justice. Search URL is https://maps.locations.airgas.com/api/getAsyncLocations?template=search&level=search&search=Kansas%20City,%20KS,%20US&radius=100000&limit=100000

#809

Other:
https://stores.pandora.net/en-au/

Existing code:
https://github.com/search?q=repo%3Aalltheplaces%2Falltheplaces%20getAsyncLocations&type=code


Raised as a meta issue, adding a storefinder that can detect /api/getAsyncLocations would be a useful tool




Discussion:

Resolving patch:
diff --git a/locations/spiders/hallmark.py b/locations/spiders/hallmark.py
--- a/locations/spiders/hallmark.py
+++ b/locations/spiders/hallmark.py
@@ -1,13 +1,7 @@
-import json
+from locations.storefinders.rio_seo_spider import RioSeoSpider
 
-import scrapy
-from scrapy.selector import Selector
 
-from locations.hours import OpeningHours
-from locations.items import Feature
-
-
-class HallmarkSpider(scrapy.Spider):
+class HallmarkSpider(RioSeoSpider):
     name = "hallmark"
     item_attributes = {"brand": "Hallmark", "brand_wikidata": "Q1521910"}
     allowed_domains = ["www.hallmark.com"]
@@ -15,44 +9,5 @@ class HallmarkSpider(scrapy.Spider):
         "https://maps.hallmark.com/api/getAsyncLocations?template=search&level=search&search=66952&radius=10000&limit=3000"
     ]
 
-    def parse(self, response):
-        maplist = json.loads(response.text)["maplist"]
-        data = json.loads("[{}]".format(Selector(text=maplist).xpath("//div/text()").extract_first()[:-1]))
-        for location in data:
-            hours = json.loads(location["hours_sets:primary"])
-            properties = {
-                "name": location["location_name"],
-                "ref": location["fid"] + "_" + location["lid"],
-                "street": (
-                    location["address_1"] + " " + location["address_2"]
-                    if location["address_2"]
-                    else location["address_1"]
-                ),
-                "city": location["city"],
-                "postcode": location["post_code"],
-                "state": location["region"],
-                "country": location["country"],
-                "phone": location["local_phone"],
-                "website": location["url"],
-                "lat": float(location["lat"]),
-                "lon": float(location["lng"]),
-                "opening_hours": self.parse_hours(hours["days"]) if hours.get("days") else None,
-            }
-            yield Feature(**properties)
-
-    def parse_hours(self, store_hours):
-        opening_hours = OpeningHours()
-
-        for weekday in store_hours:
-            day = weekday.title()
-            for interval in store_hours[weekday]:
-                if isinstance(interval, dict):
-                    open_time = str(interval.get("open"))
-                    close_time = str(interval.get("close"))
-                    opening_hours.add_range(
-                        day=day[:2],
-                        open_time=open_time,
-                        close_time=close_time,
-                        time_format="%H:%M",
-                    )
-        return opening_hours.as_opening_hours()
+    def post_process_feature(self, feature, location):
+        feature["country"] = location["country"]
diff --git a/locations/spiders/sees_candies.py b/locations/spiders/sees_candies.py
new file mode 100644
--- /dev/null
+++ b/locations/spiders/sees_candies.py
@@ -0,0 +1,14 @@
+from locations.storefinders.rio_seo_spider import RioSeoSpider
+
+
+class SeesCandiesSpider(RioSeoSpider):
+    name = "sees_candies"
+    item_attributes = {"brand": "See's Candies", "brand_wikidata": "Q2103510"}
+    allowed_domains = ["maps.sees.com"]
+    start_urls = ["https://maps.sees.com/api/getAsyncLocations?template=domain&level=domain&radius=10000&limit=3000"]
+
+    def post_process_feature(self, feature, location):
+        feature["country"] = location["country_custom"]
+        if location.get("opening_date"):
+            feature["extras"]["start_date"] = location["opening_date"]
+        feature["located_in"] = location["location_shopping_center"]
diff --git a/locations/storefinders/rio_seo_spider.py b/locations/storefinders/rio_seo_spider.py
new file mode 100644
--- /dev/null
+++ b/locations/storefinders/rio_seo_spider.py
@@ -0,0 +1,55 @@
+import json
+
+import scrapy
+from scrapy.selector import Selector
+
+from locations.hours import OpeningHours
+from locations.items import Feature
+
+
+class RioSeoSpider(scrapy.Spider):
+    def parse(self, response):
+        maplist = response.json()["maplist"]
+        data = json.loads("[{}]".format(Selector(text=maplist).xpath("//div/text()").extract_first()[:-1]))
+        for location in data:
+            hours = json.loads(location["hours_sets:primary"])
+            properties = {
+                "name": location["location_name"],
+                "ref": location["fid"] + "_" + location["lid"],
+                "street": (
+                    location["address_1"] + " " + location["address_2"]
+                    if location["address_2"]
+                    else location["address_1"]
+                ),
+                "city": location["city"],
+                "postcode": location["post_code"],
+                "state": location["region"],
+                "phone": location["local_phone"],
+                "website": location["url"],
+                "lat": float(location["lat"]),
+                "lon": float(location["lng"]),
+                "opening_hours": self.parse_hours(hours["days"]) if hours.get("days") else None,
+            }
+            feature = Feature(**properties)
+            self.post_process_feature(feature, location)
+            yield feature
+
+    def post_process_feature(self, feature, location):
+        pass
+
+    def parse_hours(self, store_hours):
+        opening_hours = OpeningHours()
+
+        for weekday in store_hours:
+            day = weekday.title()
+            for interval in store_hours[weekday]:
+                if isinstance(interval, dict):
+                    open_time = str(interval.get("open"))
+                    close_time = str(interval.get("close"))
+                    opening_hours.add_range(
+                        day=day[:2],
+                        open_time=open_time,
+                        close_time=close_time,
+                        time_format="%H:%M",
+                    )
+        return opening_hours.as_opening_hours()


Test patch: