Initial commit — site configs
- vault.hidrago.click: Vaultwarden (password manager) - git.hidrago.click: Gitea (git service) - All static sites (suma, weather, combien-jour, tokyo-trip, etc.) - Nginx configs for all subdomains - Systemd user service for Vaultwarden
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scrape sumo results from sumodb.sumogames.de and generate the Suma website."""
|
||||
|
||||
import re
|
||||
import json
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from html import unescape
|
||||
|
||||
JST = timezone(timedelta(hours=9))
|
||||
PARIS = timezone(timedelta(hours=2)) # CEST (summer)
|
||||
|
||||
BASHO_CODES = {
|
||||
"01": ("Hatsu", "初場所", "Janvier"),
|
||||
"03": ("Haru", "春場所", "Mars"),
|
||||
"05": ("Natsu", "夏場所", "Mai"),
|
||||
"07": ("Nagoya", "名古屋場所", "Juillet"),
|
||||
"09": ("Aki", "秋場所", "Septembre"),
|
||||
"11": ("Kyushu", "九州場所", "Novembre"),
|
||||
}
|
||||
|
||||
# ─── Scraping ───
|
||||
|
||||
def fetch_results(basho, day):
|
||||
"""Fetch and parse results for a given basho code (e.g. 202607) and day (1-15)."""
|
||||
url = f"https://sumodb.sumogames.de/Results.aspx?b={basho}&d={day}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (SumaBot)"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
html = resp.read().decode("utf-8", errors="replace")
|
||||
|
||||
# Header info
|
||||
h1 = re.search(r"<h1>([^<]+)</h1>", html)
|
||||
h2 = re.search(r"<h2>([^<]+)</h2>", html)
|
||||
title = h1.group(1).strip() if h1 else f"Basho {basho} Day {day}"
|
||||
date_str = h2.group(1).strip() if h2 else ""
|
||||
|
||||
# Parse tables by division
|
||||
tables = re.findall(r'<table class="tk_table".*?</table>', html, re.DOTALL)
|
||||
divisions = {}
|
||||
division_names = ["Makuuchi", "Juryo", "Makushita", "Sandanme", "Jonidan", "Jonokuchi"]
|
||||
|
||||
for i, table in enumerate(tables):
|
||||
if i >= len(division_names):
|
||||
break
|
||||
div_name = division_names[i]
|
||||
matches = re.findall(
|
||||
r'<td class="tk_kekka">(.*?)</td><td class="tk_east">(.*?)</td>'
|
||||
r'<td class="tk_kim">(.*?)</td><td class="tk_west">(.*?)</td>'
|
||||
r'<td class="tk_kekka">(.*?)</td>',
|
||||
table, re.DOTALL)
|
||||
parsed = []
|
||||
for m in matches:
|
||||
east_result_img = m[0]
|
||||
east_cell = m[1]
|
||||
kim_cell = m[2]
|
||||
west_cell = m[3]
|
||||
west_result_img = m[4]
|
||||
|
||||
# Parse wrestler info
|
||||
east = parse_wrestler(east_cell)
|
||||
west = parse_wrestler(west_cell)
|
||||
technique = parse_technique(kim_cell)
|
||||
|
||||
# Determine winner
|
||||
east_won = "shiro" in east_result_img
|
||||
west_won = "shiro" in west_result_img
|
||||
east_lost = "kuro" in east_result_img
|
||||
west_lost = "kuro" in west_result_img
|
||||
fusen = "fusen" in east_result_img or "fusen" in west_result_img
|
||||
|
||||
if east_won:
|
||||
winner = "east"
|
||||
elif west_won:
|
||||
winner = "west"
|
||||
elif fusen:
|
||||
# Check who got the fusensho (win by default)
|
||||
winner = "east" if "fusensho" in east_result_img else "west"
|
||||
else:
|
||||
winner = None
|
||||
|
||||
parsed.append({
|
||||
"east": east,
|
||||
"west": west,
|
||||
"technique": technique,
|
||||
"winner": winner,
|
||||
"fusen": fusen,
|
||||
})
|
||||
divisions[div_name] = parsed
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"date_str": date_str,
|
||||
"basho": basho,
|
||||
"day": day,
|
||||
"divisions": divisions,
|
||||
}
|
||||
|
||||
|
||||
def parse_wrestler(cell):
|
||||
"""Extract wrestler name, rank, record, and shikona from a cell."""
|
||||
rank = ""
|
||||
rank_m = re.search(r'<font size="1">(.*?)</font>', cell)
|
||||
if rank_m:
|
||||
rank = unescape(rank_m.group(1).strip())
|
||||
|
||||
# Japanese shikona from title
|
||||
shikona_ja = ""
|
||||
title_m = re.search(r"title='([^']+)'", cell)
|
||||
if title_m:
|
||||
parts = title_m.group(1).split(",")
|
||||
if parts:
|
||||
shikona_ja = parts[0].strip()
|
||||
|
||||
# English shikona from link text
|
||||
name = ""
|
||||
link_m = re.search(r"href='Rikishi\.aspx\?r=\d+'>([^<]+)</a>", cell)
|
||||
if link_m:
|
||||
name = link_m.group(1).strip()
|
||||
|
||||
# Record
|
||||
record = ""
|
||||
rec_m = re.search(r'<font color="#000000" size="1">([^<]+)</font>', cell)
|
||||
if rec_m:
|
||||
record = rec_m.group(1).strip()
|
||||
|
||||
return {"rank": rank, "name": name, "shikona_ja": shikona_ja, "record": record}
|
||||
|
||||
|
||||
def parse_technique(cell):
|
||||
"""Extract winning technique from kimari cell."""
|
||||
# Remove HTML tags
|
||||
text = re.sub(r"<[^>]+>", "", cell)
|
||||
text = unescape(text).strip()
|
||||
# The technique is on its own line (after the first <br />)
|
||||
lines = [l.strip() for l in text.split("\n") if l.strip()]
|
||||
# Filter out H2H records (contain digits)
|
||||
tech = ""
|
||||
for line in lines:
|
||||
if not re.search(r"\d", line) and line not in ("", " "):
|
||||
tech = line
|
||||
break
|
||||
return tech
|
||||
|
||||
|
||||
# ─── HTML generation ───
|
||||
|
||||
def generate_html(data):
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
now_jst = now_utc.astimezone(JST)
|
||||
now_paris = now_utc.astimezone(PARIS)
|
||||
|
||||
basho_code = data["basho"]
|
||||
month_code = basho_code[4:6]
|
||||
year = basho_code[:4]
|
||||
basho_en, basho_ja, basho_fr = BASHO_CODES.get(month_code, ("Unknown", "不明", "Inconnu"))
|
||||
|
||||
# Build division sections
|
||||
divisions_html = []
|
||||
for div_name in ["Makuuchi", "Juryo", "Makushita", "Sandanme", "Jonidan", "Jonokuchi"]:
|
||||
matches = data["divisions"].get(div_name, [])
|
||||
if not matches:
|
||||
continue
|
||||
matches_html = []
|
||||
for m in matches:
|
||||
east = m["east"]
|
||||
west = m["west"]
|
||||
winner = m["winner"]
|
||||
|
||||
# Winner/loser styling
|
||||
east_class = "winner" if winner == "east" else ("loser" if winner == "west" else "")
|
||||
west_class = "winner" if winner == "west" else ("loser" if winner == "east" else "")
|
||||
|
||||
tech_display = m["technique"] if m["technique"] and not m["fusen"] else ""
|
||||
if m["fusen"]:
|
||||
tech_display = "不戦勝" if winner == "east" else "不戦勝"
|
||||
|
||||
match_html = f""" <div class="match">
|
||||
<div class="wrestler {east_class}">
|
||||
<span class="rank">{east['rank']}</span>
|
||||
<span class="name">{east['name']}</span>
|
||||
<span class="shikona-jp">{east['shikona_ja']}</span>
|
||||
<span class="record">{east['record']}</span>
|
||||
</div>
|
||||
<div class="technique">{tech_display}</div>
|
||||
<div class="wrestler {west_class}">
|
||||
<span class="rank">{west['rank']}</span>
|
||||
<span class="name">{west['name']}</span>
|
||||
<span class="shikona-jp">{west['shikona_ja']}</span>
|
||||
<span class="record">{west['record']}</span>
|
||||
</div>
|
||||
</div>"""
|
||||
matches_html.append(match_html)
|
||||
|
||||
matches_str = "\n".join(matches_html)
|
||||
div_html = f""" <div class="division">
|
||||
<h2 class="division-title">{div_name}</h2>
|
||||
{matches_str}
|
||||
</div>"""
|
||||
divisions_html.append(div_html)
|
||||
|
||||
divisions_str = "\n".join(divisions_html)
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Suma — 相撲結果</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans JP', sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1rem;
|
||||
color: #eee;
|
||||
}}
|
||||
header {{
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: 2.5rem;
|
||||
color: #e94560;
|
||||
text-shadow: 0 2px 8px rgba(233,69,96,0.3);
|
||||
}}
|
||||
h1 .jp {{
|
||||
font-size: 1.5rem;
|
||||
color: #f5f5f5;
|
||||
margin-left: 0.5rem;
|
||||
}}
|
||||
.basho-info {{
|
||||
font-size: 1.1rem;
|
||||
color: #a8b4c5;
|
||||
margin-top: 0.3rem;
|
||||
}}
|
||||
.day-info {{
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #e94560;
|
||||
margin-top: 0.2rem;
|
||||
}}
|
||||
.date-info {{
|
||||
font-size: 0.9rem;
|
||||
color: #7a8a9e;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 680px;
|
||||
width: 100%;
|
||||
}}
|
||||
.division {{
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}}
|
||||
.division-title {{
|
||||
font-size: 1.3rem;
|
||||
color: #e94560;
|
||||
margin-bottom: 0.8rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 2px solid rgba(233,69,96,0.3);
|
||||
}}
|
||||
.match {{
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.4rem;
|
||||
overflow: hidden;
|
||||
transition: transform 0.15s;
|
||||
}}
|
||||
.match:hover {{ transform: scale(1.01); }}
|
||||
.wrestler {{
|
||||
flex: 1;
|
||||
padding: 0.6rem 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}}
|
||||
.wrestler.east {{ border-right: 1px solid rgba(255,255,255,0.08); }}
|
||||
.wrestler.winner {{
|
||||
background: rgba(76,175,80,0.12);
|
||||
}}
|
||||
.wrestler.loser {{
|
||||
opacity: 0.6;
|
||||
}}
|
||||
.wrestler .rank {{
|
||||
font-size: 0.75rem;
|
||||
color: #7a8a9e;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.wrestler .name {{
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #f5f5f5;
|
||||
}}
|
||||
.wrestler .shikona-jp {{
|
||||
font-size: 0.85rem;
|
||||
color: #a8b4c5;
|
||||
}}
|
||||
.wrestler .record {{
|
||||
font-size: 0.8rem;
|
||||
color: #8899aa;
|
||||
}}
|
||||
.wrestler.winner .record {{
|
||||
color: #66bb6a;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.technique {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #e94560;
|
||||
font-style: italic;
|
||||
min-width: 80px;
|
||||
max-width: 120px;
|
||||
text-align: center;
|
||||
background: rgba(233,69,96,0.06);
|
||||
border-left: 1px solid rgba(255,255,255,0.08);
|
||||
border-right: 1px solid rgba(255,255,255,0.08);
|
||||
}}
|
||||
.east {{ text-align: left; }}
|
||||
.west {{ text-align: right; align-items: flex-end; }}
|
||||
.winner-mark {{
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
margin-right: 6px;
|
||||
}}
|
||||
.loser-mark {{
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #e94560;
|
||||
margin-right: 6px;
|
||||
}}
|
||||
.auto-refresh {{
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: #7a8a9e;
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
.auto-refresh .pulse {{
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4caf50;
|
||||
animation: pulse 2s infinite;
|
||||
margin-right: 5px;
|
||||
}}
|
||||
@keyframes pulse {{
|
||||
0% {{ opacity: 1; }}
|
||||
50% {{ opacity: 0.3; }}
|
||||
100% {{ opacity: 1; }}
|
||||
}}
|
||||
footer {{
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #556677;
|
||||
}}
|
||||
a {{ color: #e94560; text-decoration: none; }}
|
||||
a:hover {{ text-decoration: underline; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🤼 Suma <span class="jp">相撲</span></h1>
|
||||
<div class="basho-info">{year} {basho_fr} · {basho_ja}</div>
|
||||
<div class="day-info">Jour {data['day']}</div>
|
||||
<div class="date-info">{data['date_str']}</div>
|
||||
</header>
|
||||
<div class="auto-refresh">
|
||||
<span class="pulse"></span>
|
||||
Mise à jour automatique toutes les 10 minutes
|
||||
</div>
|
||||
<div class="container">
|
||||
{divisions_str}
|
||||
</div>
|
||||
<footer>
|
||||
Données : <a href="https://sumodb.sumogames.de">sumodb.sumogames.de</a> ·
|
||||
Dernière mise à jour : {now_paris.strftime('%d/%m/%Y %H:%M')} (Paris) / {now_jst.strftime('%H:%M')} (JST)
|
||||
</footer>
|
||||
</body>
|
||||
</html>"""
|
||||
return html
|
||||
|
||||
|
||||
# ─── Auto-detect current basho and day ───
|
||||
|
||||
def detect_current_basho_day():
|
||||
"""Determine the current basho code and day based on the date."""
|
||||
now_jst = datetime.now(JST)
|
||||
year = now_jst.year
|
||||
month = now_jst.month
|
||||
|
||||
# Basho months: 1, 3, 5, 7, 9, 11
|
||||
basho_months = {1: "01", 3: "03", 5: "05", 7: "07", 9: "09", 11: "11"}
|
||||
|
||||
# Find the most recent basho
|
||||
basho_code = None
|
||||
for m in [1, 3, 5, 7, 9, 11]:
|
||||
if month >= m:
|
||||
basho_code = f"{year}{basho_months[m]}"
|
||||
elif month < 3:
|
||||
basho_code = f"{year-1}11"
|
||||
break
|
||||
|
||||
# If no basho yet this year, use last November
|
||||
if basho_code is None:
|
||||
basho_code = f"{year-1}11"
|
||||
|
||||
# Basho runs for 15 days starting on the 2nd Sunday of the month
|
||||
# For simplicity, try days 1-15 and use the latest one with data
|
||||
# First try day 15, then go down
|
||||
for day in range(15, 0, -1):
|
||||
try:
|
||||
data = fetch_results(basho_code, day)
|
||||
if data["divisions"]:
|
||||
return basho_code, day, data
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return basho_code, 1, None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) >= 3:
|
||||
basho = sys.argv[1]
|
||||
day = int(sys.argv[2])
|
||||
else:
|
||||
basho, day, pre_data = detect_current_basho_day()
|
||||
if pre_data:
|
||||
data = pre_data
|
||||
else:
|
||||
data = fetch_results(basho, day)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
# Auto mode
|
||||
if 'data' not in dir():
|
||||
data = fetch_results(basho, day)
|
||||
else:
|
||||
data = fetch_results(basho, day)
|
||||
|
||||
html = generate_html(data)
|
||||
output_path = "/home/jerome/hermes-sites/suma.hidrago.click/index.html"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
print(f"Suma page generated: basho={data['basho']} day={data['day']} -> {output_path}")
|
||||
Reference in New Issue
Block a user