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:
2026-07-31 11:38:43 +01:00
commit 486d395e6b
28 changed files with 8053 additions and 0 deletions
+462
View File
@@ -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 ("", "&nbsp;"):
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}")
File diff suppressed because it is too large Load Diff