Files
my-botty/tools/playwright_fg_scraper.py
alex 16a4d121c0 feat: log rotation with Discord notifications + failure reason tracking
- Add utils/log_rotation.py: safe_imwrite() auto-rotates screenshot dirs
  when they exceed configured file count or size limits
- Add [log_rotation] config section in params.ini (pickit/info/items
  max_files, max_mb, discord_notify_rotation)
- Replace 40 cv2.imwrite() calls with safe_imwrite() across 13 files
- Add failure reason tracking: bot.py catches run exceptions, stores
  reason in game_stats, Discord message includes the error
- Add Discord notification when log rotation deletes old files
- Prevents disk-full crashes that stopped botty on May 30
2026-05-30 09:32:24 +02:00

144 lines
5.2 KiB
Python

"""
Playwright-based headless scraper for d2jsp FG market forum.
Saves HTML files to data/d2jsp_pages/ for the offline scraper to process.
"""
import argparse
import http.cookiejar
import os
import re
import time
from pathlib import Path
from playwright.sync_api import sync_playwright
BASE_URL = "https://forums.d2jsp.org"
FORUM_PATH = "/forum.php?f=271"
BROWSER_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
"Accept-Language": "en-DK,en;q=0.9,da-DK;q=0.8,da;q=0.7,en-GB;q=0.6,en-US;q=0.5",
}
def sanitize_filename(name: str) -> str:
"""Make a safe filename from a topic title."""
bad = '<>:"/\\|?*'
for c in bad:
name = name.replace(c, "_")
return name[:120]
def main():
parser = argparse.ArgumentParser(description="Scrape d2jsp FG market forum and save HTML files.")
parser.add_argument("--max-forum-pages", type=int, default=40, help="Forum pages to scan (25 topics/page)")
parser.add_argument("--max-topics", type=int, default=800, help="Max topic pages to save")
parser.add_argument("--delay-s", type=float, default=0.5, help="Delay between requests")
parser.add_argument("--out-dir", default="data/d2jsp_pages", help="Output directory for HTML files")
parser.add_argument("--cookie-path", default="cookies.txt", help="Path to Netscape cookie jar")
args = parser.parse_args()
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent=BROWSER_HEADERS["User-Agent"],
viewport={"width": 1920, "height": 1080},
locale="en-DK",
)
# Load cookies if available
if Path(args.cookie_path).exists():
cj = http.cookiejar.MozillaCookieJar(args.cookie_path)
cj.load(ignore_discard=True, ignore_expires=True)
for cookie in cj:
context.add_cookies([{
"name": cookie.name,
"value": cookie.value,
"domain": cookie.domain,
"path": cookie.path,
}])
page = context.new_page()
# Warm up
try:
page.goto(f"{BASE_URL}/", timeout=20000, wait_until="domcontentloaded")
except Exception:
pass
topic_urls = []
for page_idx in range(args.max_forum_pages):
offset = page_idx * 25
url = f"{BASE_URL}{FORUM_PATH}&st={offset}"
try:
page.goto(url, timeout=20000, wait_until="domcontentloaded")
except Exception:
print(f"Failed to load forum page {page_idx}, stopping.")
break
# Check for 403 / Cloudflare
content = page.content()
if "Access denied" in content or "Just a moment" in content:
print("Cloudflare/403 detected, stopping forum scan.")
break
# Extract topic links using same regex as fg_market_scraper.py
links = re.findall(r'href=\"((?:/)?topic\.php\?t=\d+[^\"]*)\"', content)
seen = set()
for link in links:
# Normalize: strip leading slash, remove &v=1
norm = link.lstrip("/")
norm = re.sub(r'&v=\d+', '', norm)
if norm not in seen:
seen.add(norm)
full = f"{BASE_URL}/{norm}"
if full not in topic_urls:
topic_urls.append(full)
time.sleep(args.delay_s)
if len(topic_urls) >= args.max_topics:
break
print(f"Found {len(topic_urls)} topic URLs. Saving HTML files...")
saved = 0
skipped = 0
for i, topic_url in enumerate(topic_urls):
try:
page.goto(topic_url, timeout=20000, wait_until="domcontentloaded")
html = page.content()
# Check for Cloudflare challenge
if "Just a moment" in html or "Access denied" in html:
print(f" [{i+1}/{len(topic_urls)}] Cloudflare blocked {topic_url}")
skipped += 1
continue
# Get the topic title for filename
title_el = page.query_selector("h1")
title = title_el.inner_text().strip() if title_el else f"topic_{i}"
filename = f"[offering] {title} - Topic - d2jsp.html"
filename = sanitize_filename(filename)
filepath = out_dir / filename
filepath.write_text(html, encoding="utf-8")
saved += 1
print(f" [{i+1}/{len(topic_urls)}] Saved: {filename}")
except Exception as e:
print(f" [{i+1}/{len(topic_urls)}] Error saving {topic_url}: {e}")
skipped += 1
time.sleep(args.delay_s)
browser.close()
print(f"\nDone! Saved {saved} topics, skipped {skipped}.")
print(f"Files in {out_dir}")
print("Now run: python tools/fg_market_scraper.py --ladder-start-date YYYY-MM-DD --days 14 --offline-dir data/d2jsp_pages")
if __name__ == "__main__":
main()