Fix ffxiv_info for maintenance page

This commit is contained in:
Flawed
2026-03-02 21:42:18 -08:00
parent 78160d486e
commit 40ce9442e4
+73 -36
View File
@@ -17,8 +17,9 @@ VERSIONS_FILE = os.path.join(BASE_DIR, "automation", "ffxiv_versions_global.json
def fetch_url(url, is_json=False): def fetch_url(url, is_json=False):
print(f"Fetching {url}...", file=sys.stderr) print(f"Fetching {url}...", file=sys.stderr)
headers = { headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"accept": "application/json", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
} }
req = urllib.request.Request(url, headers=headers) req = urllib.request.Request(url, headers=headers)
try: try:
@@ -27,6 +28,22 @@ def fetch_url(url, is_json=False):
if is_json: if is_json:
return json.loads(content) return json.loads(content)
return content return content
except urllib.error.HTTPError as e:
if e.code == 503:
print(
f" Received 503 for {url}. Maintenance mode. Reading body anyway...",
file=sys.stderr,
)
try:
content = e.read().decode("utf-8")
if is_json:
return json.loads(content)
return content
except Exception:
return None
else:
print(f" HTTP Error {e.code}: {e.reason}", file=sys.stderr)
return None
except Exception as e: except Exception as e:
print(f" Error fetching {url}: {e}", file=sys.stderr) print(f" Error fetching {url}: {e}", file=sys.stderr)
return None return None
@@ -83,23 +100,29 @@ def fetch_latest_thaliak_patch() -> Optional[str]:
def parse_lodestone_news_list(html): def parse_lodestone_news_list(html):
""" """
Parses the Lodestone news category page to extract individual news items. Parses the Lodestone news list to extract news items (URL, Title, and Timestamp).
Handles the structure found on both the news category and the main landing page.
""" """
if not html: if not html:
return [] return []
# This pattern captures the link and the title following it within the same list item structure.
# It accounts for the multi-line whitespace and optional tags like [Maintenance].
# Timestamp is optional as fallback pages may not have the JS helper.
pattern = re.compile( pattern = re.compile(
r'<li class="news__list">.*?<a href="(?P<url>/lodestone/news/detail/[^"]+)"[^>]*>.*?<p class="news__list--title">(?:<span[^>]*>.*?</span>)?(?P<title>.*?)</p>.*?ldst_strftime\((?P<timestamp>\d+)', r'<a [^>]*href="(?P<url>/lodestone/news/detail/[^"]+)"[^>]*>.*?<p [^>]*class="news__list--title"[^>]*>(?P<title_content>.*?)</p>(?:.*?ldst_strftime\((?P<timestamp>\d+))?',
re.DOTALL, re.DOTALL | re.I,
) )
items = [] items = []
for match in pattern.finditer(html): for match in pattern.finditer(html):
items.append( url = "https://na.finalfantasyxiv.com" + match.group("url")
{ # Clean title content of tags and whitespace
"url": "https://na.finalfantasyxiv.com" + match.group("url"), content = match.group("title_content")
"title": match.group("title").strip(), title = re.sub(r"<[^>]+>", "", content).strip()
"timestamp": int(match.group("timestamp")), timestamp = int(match.group("timestamp")) if match.group("timestamp") else 0
} items.append({"url": url, "title": title, "timestamp": timestamp})
)
return items return items
@@ -126,42 +149,56 @@ def format_retail_version(v, count):
def scrape_latest_maintenance(): def scrape_latest_maintenance():
""" """
Scrapes the Lodestone to find the most recent 'All Worlds Maintenance' post Scrapes the Lodestone to find the most recent 'All Worlds Maintenance' post.
and counts occurrences for that specific version to determine hotfix level.
""" """
url = "https://na.finalfantasyxiv.com/lodestone/news/category/2?page=1" # Step 1: News Category
url = "https://na.finalfantasyxiv.com/lodestone/news/category/2"
html = fetch_url(url) html = fetch_url(url)
# Step 2: Fallback to Landing Page (common during 503 maintenance)
if not html:
url = "https://na.finalfantasyxiv.com/lodestone/"
html = fetch_url(url)
if not html: if not html:
return None return None
news_items = parse_lodestone_news_list(html) news_items = parse_lodestone_news_list(html)
maintenance_log = []
# Identify all relevant maintenance posts on the first page
for item in news_items: for item in news_items:
if is_maintenance_post(item["title"]): title = item["title"]
v = extract_patch_version(fetch_url(item["url"])) link = item["url"]
if v:
item["version"] = v if not is_maintenance_post(title):
maintenance_log.append(item) continue
detail_html = fetch_url(link)
version = extract_patch_version(detail_html)
if version:
# We assume the first (most recent) valid maintenance post
# with a patch version is our target.
return {"retail_version": version, "title": title, "url": link}
if not maintenance_log:
return None return None
# Assumes hotfixes appear as separate maintenance posts with the same
# retail version.
counts = {}
for item in reversed(maintenance_log):
v = item["version"]
counts[v] = counts.get(v, 0) + 1
item["retail_version"] = format_retail_version(v, counts[v])
latest = maintenance_log[0] def get_version_info() -> Optional[dict]:
print( """
f" Final Scrape Result: {latest['title']} -> {latest['retail_version']}", Returns the latest version information by combining Thaliak and Lodestone data.
file=sys.stderr, """
) date_new = fetch_latest_thaliak_patch()
return latest maintenance = scrape_latest_maintenance()
if not maintenance and not date_new:
return None
return {
"retail_version": maintenance["retail_version"] if maintenance else "Unknown",
"version_string": date_new,
"title": maintenance["title"] if maintenance else "Unknown",
"url": maintenance["url"] if maintenance else "Unknown",
}
def get_patch_context(): def get_patch_context():