Update api/main.py
All checks were successful
Build & Push Football Docker Images / build-push-update (push) Successful in 7s
All checks were successful
Build & Push Football Docker Images / build-push-update (push) Successful in 7s
Clean up
This commit is contained in:
117
api/main.py
117
api/main.py
@@ -4,11 +4,12 @@ FC Porto fixtures API
|
|||||||
Endpoints:
|
Endpoints:
|
||||||
GET /health — liveness
|
GET /health — liveness
|
||||||
GET /data — raw JSON
|
GET /data — raw JSON
|
||||||
GET /next — next match (for Homepage customapi)
|
GET /next — next match
|
||||||
GET /widget — self-contained HTML widget for Homepage iframe
|
GET /widget — HTML widget for Homepage iframe
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
@@ -94,87 +95,66 @@ def next_match():
|
|||||||
|
|
||||||
@app.get("/widget", response_class=HTMLResponse)
|
@app.get("/widget", response_class=HTMLResponse)
|
||||||
def widget():
|
def widget():
|
||||||
"""
|
|
||||||
Layout: 3 past results + 2 upcoming fixtures
|
|
||||||
|
|
||||||
For each match card:
|
|
||||||
[ home badge ]
|
|
||||||
[ home name ]
|
|
||||||
[ score/date ]
|
|
||||||
[ away badge ]
|
|
||||||
[ away name ]
|
|
||||||
[ comp abbr ]
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
d = load()
|
d = load()
|
||||||
past, future = sorted_matches(d)
|
past, future = sorted_matches(d)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
past, future = [], []
|
past, future = [], []
|
||||||
|
|
||||||
# Last 3 past + next 2 future
|
# Most recent 3 past (newest leftmost = reversed) + next 2 future
|
||||||
last3 = past[-3:] if len(past) >= 3 else past
|
last3 = list(reversed(past[-3:])) if len(past) >= 3 else list(reversed(past))
|
||||||
next2 = future[:2] if len(future) >= 2 else future
|
next2 = future[:2] if len(future) >= 2 else future
|
||||||
|
|
||||||
# Pad with empties
|
empty_past = {"home": "—", "away": "—", "home_logo": None, "away_logo": None,
|
||||||
empty = {
|
"abbr": "—", "date": "—", "time": "", "score": None}
|
||||||
"home": "—", "away": "—",
|
empty_future = {**empty_past, "score": None}
|
||||||
"home_logo": None, "away_logo": None,
|
|
||||||
"competition": "—", "abbr": "—",
|
|
||||||
"date": "—", "time": "", "score": None, "is_past": True,
|
|
||||||
}
|
|
||||||
while len(last3) < 3:
|
|
||||||
last3.insert(0, dict(empty))
|
|
||||||
while len(next2) < 2:
|
|
||||||
next2.append(dict(empty, is_past=False))
|
|
||||||
|
|
||||||
all_cards = last3 + next2
|
while len(last3) < 3:
|
||||||
|
last3.append(dict(empty_past))
|
||||||
|
while len(next2) < 2:
|
||||||
|
next2.append(dict(empty_future))
|
||||||
|
|
||||||
PORTO_LOGO = "https://a.espncdn.com/i/teamlogos/soccer/500/437.png"
|
PORTO_LOGO = "https://a.espncdn.com/i/teamlogos/soccer/500/437.png"
|
||||||
|
|
||||||
def card(m: dict, is_past: bool) -> str:
|
def clean_time(t: str) -> str:
|
||||||
home = m.get("home", "—")
|
"""Remove ESPN artifacts like 'v', 'v2nd Leg' etc from time field."""
|
||||||
away = m.get("away", "—")
|
t = t.strip()
|
||||||
home_logo = m.get("home_logo") or PORTO_LOGO
|
# If it looks like a time (contains :) keep it, otherwise TBD
|
||||||
away_logo = m.get("away_logo") or PORTO_LOGO
|
if re.search(r'\d+:\d+', t):
|
||||||
abbr = m.get("abbr", "—")
|
return re.search(r'\d+:\d+\s*(?:AM|PM)?', t).group(0).strip()
|
||||||
score = m.get("score")
|
return "TBD"
|
||||||
date = m.get("date", "—")
|
|
||||||
time_ = m.get("time", "")
|
|
||||||
time_str = m.get("time", "")
|
|
||||||
|
|
||||||
|
|
||||||
time_str = re.sub(r'^v\s*', '', time_str).strip()
|
|
||||||
time_str = time_str if time_str not in ("", "TBD", "v") else "TBD"
|
|
||||||
|
|
||||||
|
|
||||||
# Shorten names for display
|
|
||||||
def shorten(name: str) -> str:
|
def shorten(name: str) -> str:
|
||||||
if len(name) <= 10:
|
if len(name) <= 10:
|
||||||
return name
|
return name
|
||||||
parts = name.split()
|
parts = name.split()
|
||||||
return parts[-1] if parts else name[:10]
|
return parts[-1] if parts else name[:10]
|
||||||
|
|
||||||
home_s = shorten(home)
|
def card(m: dict, is_past: bool) -> str:
|
||||||
away_s = shorten(away)
|
home_logo = m.get("home_logo") or PORTO_LOGO
|
||||||
|
away_logo = m.get("away_logo") or PORTO_LOGO
|
||||||
|
home_s = shorten(m.get("home", "—"))
|
||||||
|
away_s = shorten(m.get("away", "—"))
|
||||||
|
abbr = m.get("abbr", "—")
|
||||||
|
score = m.get("score")
|
||||||
|
date = m.get("date", "—")
|
||||||
|
time_raw = m.get("time", "")
|
||||||
|
|
||||||
if is_past and score:
|
if is_past and score:
|
||||||
middle = f'<div class="score">{score}</div>'
|
middle = f'<div class="score">{score}</div>'
|
||||||
cls = "card past"
|
cls = "card past"
|
||||||
else:
|
else:
|
||||||
date_s = "/".join(date.split("/")[:2]) if "/" in date else date
|
date_s = "/".join(date.split("/")[:2]) if "/" in date else date
|
||||||
time_part = f"<br>{time_str}" if time_str and time_str != "TBD" else ""
|
time_clean = clean_time(time_raw)
|
||||||
|
time_part = f"<br>{time_clean}" if time_clean != "TBD" else ""
|
||||||
middle = f'<div class="fixture">{date_s}{time_part}</div>'
|
middle = f'<div class="fixture">{date_s}{time_part}</div>'
|
||||||
|
|
||||||
time_s = f"<br>{time_}" if time_ and time_ != "TBD" else ""
|
|
||||||
|
|
||||||
cls = "card future"
|
cls = "card future"
|
||||||
|
|
||||||
return f"""
|
return f"""<div class="{cls}">
|
||||||
<div class="{cls}">
|
<img src="{home_logo}" class="badge" alt="">
|
||||||
<img src="{home_logo}" class="badge" alt="{home}">
|
|
||||||
<div class="name home-name">{home_s}</div>
|
<div class="name home-name">{home_s}</div>
|
||||||
{middle}
|
{middle}
|
||||||
<img src="{away_logo}" class="badge" alt="{away}">
|
<img src="{away_logo}" class="badge" alt="">
|
||||||
<div class="name away-name">{away_s}</div>
|
<div class="name away-name">{away_s}</div>
|
||||||
<div class="comp">{abbr}</div>
|
<div class="comp">{abbr}</div>
|
||||||
</div>"""
|
</div>"""
|
||||||
@@ -197,6 +177,12 @@ def widget():
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}}
|
}}
|
||||||
|
.container {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 4px;
|
||||||
|
}}
|
||||||
.section {{
|
.section {{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -205,33 +191,26 @@ def widget():
|
|||||||
.divider {{
|
.divider {{
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: #334155;
|
background: #334155;
|
||||||
margin: 4px 6px;
|
margin: 4px 8px;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}}
|
}}
|
||||||
.container {{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 4px;
|
|
||||||
}}
|
|
||||||
.card {{
|
.card {{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 2px 4px;
|
padding: 2px 6px;
|
||||||
gap: 1px;
|
gap: 2px;
|
||||||
min-width: 0;
|
min-width: 60px;
|
||||||
}}
|
}}
|
||||||
.past {{ opacity: 0.7; }}
|
.past {{ opacity: 0.7; }}
|
||||||
.future {{ opacity: 1; }}
|
.future {{ opacity: 1; }}
|
||||||
.badge {{
|
.badge {{
|
||||||
width: 22px;
|
width: 24px;
|
||||||
height: 22px;
|
height: 24px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
flex-shrink: 0;
|
|
||||||
}}
|
}}
|
||||||
.name {{
|
.name {{
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
@@ -245,16 +224,16 @@ def widget():
|
|||||||
.home-name {{ color: #cbd5e1; }}
|
.home-name {{ color: #cbd5e1; }}
|
||||||
.away-name {{ color: #94a3b8; }}
|
.away-name {{ color: #94a3b8; }}
|
||||||
.score {{
|
.score {{
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #4ade80;
|
color: #4ade80;
|
||||||
padding: 1px 0;
|
padding: 2px 0;
|
||||||
}}
|
}}
|
||||||
.fixture {{
|
.fixture {{
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #93c5fd;
|
color: #93c5fd;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.3;
|
line-height: 1.4;
|
||||||
}}
|
}}
|
||||||
.comp {{
|
.comp {{
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user