From 647305365dfba43a4cf03b92960e724a2e5d8369 Mon Sep 17 00:00:00 2001 From: javara999 Date: Fri, 17 Apr 2026 12:04:50 +0900 Subject: [PATCH] stove bug --- __pycache__/scraper.cpython-310.pyc | Bin 0 -> 24281 bytes scraper.py | 260 ++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 __pycache__/scraper.cpython-310.pyc diff --git a/__pycache__/scraper.cpython-310.pyc b/__pycache__/scraper.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..802832438b7685d09dec8951b164d6db113494c2 GIT binary patch literal 24281 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 literal 0 HcmV?d00001 diff --git a/scraper.py b/scraper.py index 414f9e7..b1ffded 100644 --- a/scraper.py +++ b/scraper.py @@ -573,7 +573,267 @@ def _is_stove_demo_game(store_url): return bool(re.search(r">\s*DEMO\s*<", html, re.IGNORECASE)) +def fetch_stove_free(): + try: + import json + from bs4 import BeautifulSoup + except Exception: + log.warning("BeautifulSoup not installed; STOVE skipped") + return [] + + def _abs_url(url): + if not url: + return "" + if url.startswith("http://") or url.startswith("https://"): + return url + if url.startswith("//"): + return "https:" + url + return "https://store.onstove.com" + url + + def _to_float(value): + if value is None: + return 0.0 + if isinstance(value, (int, float)): + return float(value) + text = re.sub(r"[^0-9.]+", "", str(value)) + if not text: + return 0.0 + try: + return float(text) + except Exception: + return 0.0 + + def _price_values(text): + values = [] + for match in re.finditer(r"(?:USD|KRW|[₩$])\s*([0-9][0-9,]*(?:\.[0-9]+)?)", text): + try: + values.append(float(match.group(1).replace(",", ""))) + except Exception: + pass + return values + + def _iter_dicts(node): + if isinstance(node, dict): + yield node + for value in node.values(): + for item in _iter_dicts(value): + yield item + elif isinstance(node, list): + for value in node: + for item in _iter_dicts(value): + yield item + + def _pick(dct, keys): + for key in keys: + value = dct.get(key) + if value not in (None, "", [], {}): + return value + return None + + def _store_url(game_id, data): + url = _pick(data, ["storeUrl", "store_url", "productUrl", "product_url", "url"]) + if url: + return _abs_url(str(url).strip()) + return "https://store.onstove.com/ko/games/" + str(game_id) + + def _build_game(data): + game_id = _pick(data, ["productNo", "productId", "product_id", "id", "no"]) + if game_id in (None, ""): + return None + game_id = str(game_id).strip() + title = _pick(data, ["productName", "name", "title", "product_name"]) + if not title: + return None + title = " ".join(str(title).split()) + if _DEMO_TITLE_RE.search(title): + return None + + current_price = _to_float(_pick(data, ["salePrice", "discountPrice", "currentPrice", "finalPrice", "price", "sellingPrice", "sale_price"])) + original_price = _to_float(_pick(data, ["originPrice", "originalPrice", "listPrice", "basePrice", "priceBeforeDiscount", "normalPrice", "origin_price"])) + if original_price <= 0.0 or current_price != 0.0: + return None + + image_url = _pick(data, ["imageUrl", "image_url", "thumbnailUrl", "thumbnail_image_url", "verticalImageUrl", "horizontalImageUrl", "coverImageUrl"]) or "" + return { + "external_id": "stove_" + game_id, + "platform": "stove", + "title": title, + "image_url": _abs_url(str(image_url).strip()), + "store_url": _store_url(game_id, data), + "original_price": original_price, + "current_price": 0.0, + "discount_pct": 100, + "is_free_period": True, + "free_start": None, + "free_end": None, + "genres": [], + "rating": 0.0, + "rating_count": 0, + } + + def _collect_games(payload, seen): + games = [] + for item in _iter_dicts(payload): + game = _build_game(item) + if not game: + continue + if game["external_id"] in seen: + continue + seen.add(game["external_id"]) + games.append(game) + return games + + def _extract_json_from_html(html): + payloads = [] + for match in re.finditer(r']+type=["\']application/json["\'][^>]*>(.*?)', html, re.I | re.S): + raw = match.group(1).strip() + if not raw: + continue + try: + payloads.append(json.loads(raw)) + except Exception: + pass + for pattern in [r"__NUXT__\s*=\s*(\{.*?\})\s*", r"window\.__STORE__\s*=\s*(\{.*?\})\s*;"]: + for match in re.finditer(pattern, html, re.I | re.S): + try: + payloads.append(json.loads(match.group(1))) + except Exception: + pass + return payloads + + def _stove_get(session, url, html=False, **kwargs): + headers = _HTML_HEADERS if html else _HEADERS + if "headers" not in kwargs: + kwargs["headers"] = headers + resp = session.get(url, timeout=_TIMEOUT, allow_redirects=True, **kwargs) + resp.raise_for_status() + return resp + + api_headers = dict(_HEADERS, Referer="https://store.onstove.com/", Accept="application/json") + html_headers = dict(_HTML_HEADERS, Referer="https://store.onstove.com/") + api_urls = [ + "https://store.onstove.com/api/v2/product/list?product_type=GAME&price_type=FREE&page=1&size=20", + "https://api.onstove.com/store/v2/product/list?product_type=GAME&price_type=FREE", + "https://store.onstove.com/api/store/v2/event/freegame", + ] + page_urls = [ + "https://store.onstove.com/ko/games?priceFilter=FREE", + "https://store.onstove.com/ko/promotions", + ] + results = [] + seen = set() + session = requests.Session() + + try: + _stove_get(session, "https://store.onstove.com/", html=True, headers=html_headers) + except Exception as e: + log.warning("STOVE session bootstrap failed: %s", e) + + for api_url in api_urls: + try: + payload = _stove_get(session, api_url, headers=api_headers).json() + except Exception as e: + log.warning("STOVE API fetch failed: %s (%s)", api_url, e) + continue + results.extend(_collect_games(payload, seen)) + if results: + log.info("STOVE free: %d game(s) found via API", len(results)) + return results + + for page_url in page_urls: + try: + html = _stove_get(session, page_url, html=True, headers=html_headers).text + except Exception as e: + log.warning("STOVE page fetch failed: %s (%s)", page_url, e) + continue + + for payload in _extract_json_from_html(html): + results.extend(_collect_games(payload, seen)) + if results: + log.info("STOVE free: %d game(s) found via embedded JSON", len(results)) + return results + + soup = BeautifulSoup(html, "html.parser") + for anchor in soup.find_all("a", href=True): + href = str(anchor.get("href") or "").strip() + match = re.search(r"/games/(\d+)", href) + if not match: + continue + game_id = match.group(1) + external_id = "stove_" + game_id + if external_id in seen: + continue + + block = anchor + for _ in range(5): + text = " ".join(block.stripped_strings) + prices = _price_values(text) + if "-100%" in text and prices and prices[-1] == 0.0 and any(value > 0.0 for value in prices[:-1]): + break + if block.parent is None: + block = None + break + block = block.parent + if block is None: + continue + + title = "" + for tag in block.find_all(["h1", "h2", "h3", "h4", "strong"]): + title = " ".join(tag.get_text(" ", strip=True).split()) + if title: + break + if not title: + title = game_id + if _DEMO_TITLE_RE.search(title): + continue + + prices = _price_values(" ".join(block.stripped_strings)) + if len(prices) < 2: + continue + original_price = 0.0 + for value in prices[:-1]: + original_price = _to_float(value) + if original_price > 0.0: + break + if original_price <= 0.0 or _to_float(prices[-1]) != 0.0: + continue + + image_url = "" + image_node = block.find("img") or anchor.find("img") + if image_node is not None: + image_url = image_node.get("src") or image_node.get("data-src") or image_node.get("data-lazy-src") or "" + + seen.add(external_id) + results.append({ + "external_id": external_id, + "platform": "stove", + "title": title, + "image_url": _abs_url(str(image_url).strip()), + "store_url": _abs_url(href), + "original_price": original_price, + "current_price": 0.0, + "discount_pct": 100, + "is_free_period": True, + "free_start": None, + "free_end": None, + "genres": [], + "rating": 0.0, + "rating_count": 0, + }) + + if results: + log.info("STOVE free: %d game(s) found via HTML fallback", len(results)) + return results + + log.info("STOVE free: %d game(s) found", len(results)) + return results + + def fetch_stove_deals(): + return fetch_stove_free() + + +def _fetch_stove_deals_legacy(): try: from bs4 import BeautifulSoup resp = _get("https://store.onstove.com/ko/store/stoveindie", html=True)