From 3b4bb5aa5c2ed05817af602fb1e6ad2069ada12a Mon Sep 17 00:00:00 2001 From: prettysunflower Date: Tue, 24 Jun 2025 21:54:53 -0400 Subject: [PATCH] Added marketplace availability listing --- surugaya_api/japan_prefectures.py | 51 +++++++++++++ surugaya_api/product.py | 120 +++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/surugaya_api/japan_prefectures.py b/surugaya_api/japan_prefectures.py index e69de29..8175058 100644 --- a/surugaya_api/japan_prefectures.py +++ b/surugaya_api/japan_prefectures.py @@ -0,0 +1,51 @@ +from enum import Enum + + +class JapanesePrefectures(Enum): + AICHI = "愛知県" + AKITA = "秋田県" + AOMORI = "青森県" + CHIBA = "千葉県" + EHIME = "愛媛県" + FUKUI = "福井県" + FUKUOKA = "福岡県" + FUKUSHIMA = "福島県" + GIFU = "岐阜県" + GUNMA = "群馬県" + HIROSHIMA = "広島県" + HOKKAIDO = "北海道" + HYOGO = "兵庫県" + IBARAKI = "茨城県" + ISHIKAWA = "石川県" + IWATE = "岩手県" + KAGAWA = "香川県" + KAGOSHIMA = "鹿児島県" + KANAGAWA = "神奈川県" + KOCHI = "高知県" + KUMAMOTO = "熊本県" + KYOTO = "京都府" + MIE = "三重県" + MIYAGI = "宮城県" + MIYAZAKI = "宮崎県" + NAGANO = "長野県" + NAGASAKI = "長崎県" + NARA = "奈良県" + NIIGATA = "新潟県" + OITA = "大分県" + OKAYAMA = "岡山県" + OKINAWA = "沖縄県" + OSAKA = "大阪府" + SAGA = "佐賀県" + SAITAMA = "埼玉県" + SHIGA = "滋賀県" + SHIMANE = "島根県" + SHIZUOKA = "静岡県" + TOCHIGI = "栃木県" + TOKUSHIMA = "徳島県" + TOKYO = "東京都" + TOTTORI = "鳥取県" + TOYAMA = "富山県" + WAKAYAMA = "和歌山県" + YAMAGATA = "山形県" + YAMAGUCHI = "山口県" + YAMANASHI = "山梨県" diff --git a/surugaya_api/product.py b/surugaya_api/product.py index 7a2bbf1..5de6b6e 100644 --- a/surugaya_api/product.py +++ b/surugaya_api/product.py @@ -9,6 +9,9 @@ from surugaya_api.consts import SURU_COOKIE_STRING import inspect +from surugaya_api.japan_prefectures import JapanesePrefectures + + def get_or_create_aiohttp_session(func): async def wrapper(*args, **kwargs): sig = inspect.signature(func) @@ -31,6 +34,8 @@ def get_or_create_aiohttp_session(func): return result return wrapper + + @dataclass class ProductStock: condition: str @@ -50,9 +55,10 @@ class ProductStock: condition=item_price.select_one("label").attrs["data-label"].strip(), stock=data["zaiko"], price=data["baika"], - on_sale_price=data.get("price_sale") + on_sale_price=data.get("price_sale"), ) + @dataclass class Product: id: int @@ -89,3 +95,115 @@ async def load_product(product_id, aiohttp_session=None): return product +@dataclass +class MarketplaceListing: + product_id: int + item_price: int + condition: str + product_link: str + store_name: str + store_id: int + special_info: str + shipping_price: int + + @property + def total_price(self) -> int: + return self.item_price + self.shipping_price + + +@get_or_create_aiohttp_session +async def get_shipping_price_for_store( + store_id, + to_prefecture=JapanesePrefectures.KANAGAWA, + aiohttp_session: Optional[aiohttp.ClientSession] = None, +) -> int: + response = await aiohttp_session.post( + url="https://www.suruga-ya.jp/product-other/shipping-fee", + headers={ + "Accept": "application/json, text/javascript, */*; q=0.01", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Origin": "https://www.suruga-ya.jp", + "Cookie": SURU_COOKIE_STRING, + }, + data={ + "tenpo_cd": store_id, + }, + ) + + response_json = await response.json() + shipping_fees = response_json["data"] + + if not shipping_fees["list_pref_fee"]: + shipping_price = shipping_fees["shipping"]["national_fee"] + else: + shipping_filtered = [ + x + for x in shipping_fees["list_pref_fee"] + if x["prefecture"] == to_prefecture.value + ] + + if not shipping_filtered: + shipping_price = shipping_fees["shipping"]["national_fee"] + else: + shipping_price = shipping_filtered[0]["fee"] + + return int(shipping_price) + + +@get_or_create_aiohttp_session +async def get_marketplace_availability( + product_id, + shipping_to_prefecture=JapanesePrefectures.KANAGAWA, + aiohttp_session: Optional[aiohttp.ClientSession] = None, +): + async with aiohttp_session.get( + f"https://www.suruga-ya.jp/product/other/{product_id}" + ) as response: + if response.status == 301 or response.status == 302: + # TODO: Handle redirect to /product/detail if only one store is available + return [] + + page = await response.text() + page_bs = BeautifulSoup(page, features="html.parser") + + items = page_bs.select("#tbl_all tr.item") + listings = [] + + for item in items: + if item.select_one("td:nth-child(3) a").attrs["href"] == "/": + # Main online store, not processing + continue + + item_price = ( + item.select_one("td:nth-child(1) .title_product > strong") + .text.replace(",", "") + .replace("円", "") + ) + + condition = item.select_one("td:nth-child(2) .title_product").text + product_link = item.select_one("td:nth-child(2) > a").attrs["href"] + + store_id = item.select_one("td:nth-child(3) a").attrs["href"].split("/")[-1] + store_name = item.select_one("td:nth-child(3) a").text + shipping_price = await get_shipping_price_for_store( + store_id, shipping_to_prefecture, aiohttp_session + ) + + special_info = "\n".join( + [x.text for x in item.select("td:nth-child(4) .padT5 div")] + ) + + listings.append( + MarketplaceListing( + product_id=int(product_id), + item_price=int(item_price), + condition=condition, + product_link=product_link, + store_name=store_name, + store_id=int(store_id), + special_info=special_info, + shipping_price=shipping_price, + ) + ) + + return listings