fix(mls): ESPN 403 — 브라우저 위장 UA 제거
컨테이너(httpx)에서 풀 Chrome UA 를 보내면 ESPN WAF 가 403 차단 (TLS 핑거프린트-UA 불일치 감지로 추정). httpx 기본 UA 는 통과 — 일정 동기화가 계속 실패해 로컬에서 MLS 일정이 안 뜨던 원인. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0415ab7636
commit
13a4997cc2
@ -29,12 +29,8 @@ from ..teams_mls import MLS_ID_TO_CODE, MLS_TEAMS
|
|||||||
log = logging.getLogger("triplepick.mls")
|
log = logging.getLogger("triplepick.mls")
|
||||||
|
|
||||||
KST = timezone(timedelta(hours=9))
|
KST = timezone(timedelta(hours=9))
|
||||||
UA = {
|
# 주의: ESPN 은 브라우저 위장 UA(풀 Chrome 문자열)를 보내면 WAF 가 403 으로 차단한다
|
||||||
"User-Agent": (
|
# (TLS 핑거프린트와 UA 불일치 감지로 추정). httpx 기본 UA 로 보내야 통과 — 헤더 없이 호출.
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
||||||
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 취소로 취급하는 ESPN 상태 (연기 포함 — 보강 일정이 새 이벤트로 재등장)
|
# 취소로 취급하는 ESPN 상태 (연기 포함 — 보강 일정이 새 이벤트로 재등장)
|
||||||
_CANCEL_STATUS = {"STATUS_POSTPONED", "STATUS_CANCELED", "STATUS_ABANDONED"}
|
_CANCEL_STATUS = {"STATUS_POSTPONED", "STATUS_CANCELED", "STATUS_ABANDONED"}
|
||||||
@ -94,7 +90,7 @@ def _parse_event(e: dict) -> dict | None:
|
|||||||
|
|
||||||
|
|
||||||
async def _fetch_events(client, dates: str) -> list[dict]:
|
async def _fetch_events(client, dates: str) -> list[dict]:
|
||||||
r = await client.get(_sb_url(dates), headers=UA)
|
r = await client.get(_sb_url(dates))
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
out = []
|
out = []
|
||||||
for e in r.json().get("events") or []:
|
for e in r.json().get("events") or []:
|
||||||
@ -148,7 +144,7 @@ async def _refresh_standings(db) -> bool:
|
|||||||
table: dict[str, dict] = {}
|
table: dict[str, dict] = {}
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=15) as c:
|
async with httpx.AsyncClient(timeout=15) as c:
|
||||||
r = await c.get(_standings_url(), headers=UA)
|
r = await c.get(_standings_url())
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
for conf in data.get("children") or []:
|
for conf in data.get("children") or []:
|
||||||
@ -263,7 +259,7 @@ async def _refresh_previews(db, matches: list[Match]) -> int:
|
|||||||
"odds": _odds_of(rec["_comp"]),
|
"odds": _odds_of(rec["_comp"]),
|
||||||
}
|
}
|
||||||
try: # 맞대결(h2h)은 summary 1콜 — 실패해도 나머지 프리뷰는 유지
|
try: # 맞대결(h2h)은 summary 1콜 — 실패해도 나머지 프리뷰는 유지
|
||||||
r2 = await c.get(_summary_url(rec["espnId"]), headers=UA)
|
r2 = await c.get(_summary_url(rec["espnId"]))
|
||||||
r2.raise_for_status()
|
r2.raise_for_status()
|
||||||
payload["h2h"] = _h2h_of(r2.json())
|
payload["h2h"] = _h2h_of(r2.json())
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
@ -598,7 +594,7 @@ async def fetch_live_mls(m: Match) -> dict:
|
|||||||
_rn = _make_name_resolver(None) # summary 실패 시 원문 그대로
|
_rn = _make_name_resolver(None) # summary 실패 시 원문 그대로
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=15) as c2:
|
async with httpx.AsyncClient(timeout=15) as c2:
|
||||||
r2 = await c2.get(_summary_url(rec["espnId"]), headers=UA)
|
r2 = await c2.get(_summary_url(rec["espnId"]))
|
||||||
r2.raise_for_status()
|
r2.raise_for_status()
|
||||||
sj = r2.json()
|
sj = r2.json()
|
||||||
lineups = _lineups_of(sj)
|
lineups = _lineups_of(sj)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user