mcpField
T
thscore-football-api

thscore-football-api

github.com/silkyland開発158 回インストール34 週間前に更新
リポジトリ

thscore-football-api について

thscore-football-api は、THScore Football API の連携に向けた開発スキルで、TypeScript の型、エンドポイント例、実装ルールを提供します。ライブスコア、日程、統計、オッズ、過去データを扱い、キャッシュ、エラー処理、API キー保護にも触れています。

主な機能

  1. 01TypeScript の型とエンドポイント例
  2. 02ライブスコア・日程・試合統計の連携
  3. 03リーグ情報・順位表・得点ランキング
  4. 04オッズフィードと過去データの利用
  5. 05キャッシュ・エラー処理・キー保護の指針

活用シーン

  1. 01THScore API クライアントや HTTP ラッパーの構築
  2. 02ライブスコア・試合アニメーション・順位表の開発
  3. 03過去の試合データ同期とキャッシュ設計
  4. 04API キー・レート制限・応答形式の問題調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/silkyland/football-api-skill --skill thscore-football-api

インストール後、Claude Code で/thscore-football-apiを入力して読み込まれたか確認します。

同梱ファイルtypes.ts

詳細

THScore Football API – 開発者向けスキル

THScore Football API を利用するための包括的なベストプラクティスとリファレンスガイドです。このスキルには、AI エージェントを活用した統合を信頼性の高い、本番環境対応のものにするために設計された TypeScript の型定義、エンドポイントのスタブ、ルール、完全なコード例が含まれています。


適用する場面

次のような場合に、このスキルを参照してください。

  • THScore API クライアントや HTTP ラッパーをセットアップするとき
  • ライブスコア、試合のタイムライン、または試合中の統計データを取得するとき
  • リーグ/カップの参照データ(国、リーグ ID、ルール)を取得するとき
  • サッカーのライブアニメーション(iFrame/WebView 埋め込み)を表示するとき
  • 順位表、得点ランキング、または FIFA ランキングページを作成するとき
  • アジアンハンディキャップ、ヨーロッパ、または 1×2 オッズのフィードを統合するとき
  • 過去の試合データを同期するスケジュール済みジョブを作成するとき
  • スポーツデータ用のキャッシュレイヤーやデータベーススキーマを設計するとき
  • API キーエラー、レート制限違反、または不正なレスポンスをデバッグするとき

利用可能な製品プランとエンドポイントセット

プランAPI 数主な機能
Live Data ($599/mo)22ライブスコア、日程、イベント、ラインナップ、ライブテキスト、プロフィール
Stats ($399/mo)13試合分析、選手統計、順位表、得点ランキング、FIFA ランキング
Odds—18 のブックメーカー、アジアンハンディキャップ、ヨーロッパ、1×2
Odds Pro—200 以上のヨーロッパのブックメーカー
Betfair—Betfair の取引所データ
Live Animation ($1699/mo)1iFrame/WebView アニメーション、42 種類のイベント
Historical Database—過去のシーズン、過去の試合データ
Common API∞すべての有料プランで共通:リーグ・カップのプロフィール、国、ブックメーカー

ベース URL と認証

http://www.thscore.info/<API_PATH>?api_key=<YOUR_API_KEY>

アジアに設置されたサーバーの場合(レイテンシーを低減):

http://api2.thscore.info/<API_PATH>?api_key=<YOUR_API_KEY>

api_key は必ず環境変数に保存し、ハードコードしないでください。


TypeScript の型(types.ts)

// ==========================================================
// types.ts
// THScore Football API – Complete TypeScript Type Definitions
// ==========================================================

// ── Common / Shared ────────────────────────────────────────

/** Unix timestamp (seconds) */
type UnixTimestamp = number;

/** ISO 3166-1 alpha-2 country code */
type CountryCode = string;

/** Match state integer returned by Livescores */
export enum MatchState {
  NotStarted    = 0,
  FirstHalf     = 1,
  HalfTime      = 2,
  SecondHalf    = 3,
  ExtraTimeFirst = 4,
  ExtraTimeHT   = 5,
  ExtraTimeSecond = 6,
  PenaltyShootout = 7,
  Finished      = -1,
  Postponed     = -11,
  Cancelled     = -12,
  TBD           = -13,
  Abandoned     = -14,
  Suspended     = -15,
  Delayed       = -16,
  Interrupted   = -17,
  ExtraTime     = -18,
  Walkover      = -19,
}

/** Match state labels (use to display to end users) */
export const MatchStateLabel: Record<MatchState, string> = {
  [MatchState.NotStarted]:       "Not Started",
  [MatchState.FirstHalf]:        "1st Half",
  [MatchState.HalfTime]:         "Half Time",
  [MatchState.SecondHalf]:       "2nd Half",
  [MatchState.ExtraTimeFirst]:   "Extra Time 1st",
  [MatchState.ExtraTimeHT]:      "Extra Time HT",
  [MatchState.ExtraTimeSecond]:  "Extra Time 2nd",
  [MatchState.PenaltyShootout]:  "Penalties",
  [MatchState.Finished]:         "Finished",
  [MatchState.Postponed]:        "Postponed",
  [MatchState.Cancelled]:        "Cancelled",
  [MatchState.TBD]:              "TBD",
  [MatchState.Abandoned]:        "Abandoned",
  [MatchState.Suspended]:        "Suspended",
  [MatchState.Delayed]:          "Delayed",
  [MatchState.Interrupted]:      "Interrupted",
  [MatchState.ExtraTime]:        "Extra Time",
  [MatchState.Walkover]:         "Walkover",
};

// ── Common API ─────────────────────────────────────────────
// GET /football_th/league/basic.aspx (1 hr limit / 1 day recommended)

export interface LeagueBasic {
  leagueId:      number;
  leagueName:    string;
  leagueNameEn:  string;
  countryId:     number;
  countryName:   string;
  countryNameEn: string;
  leagueLogo:    string;  // URL
  countryLogo:   string;  // URL
  type:          number;  // 1 = League, 2 = Cup, 3 = International
  curSeasonId:   number;
}

export interface LeagueBasicResponse {
  code:    number;
  message: string;
  results: LeagueBasic[];
}

// GET /football_th/league.aspx (30 min limit / 1 day recommended)

export interface LeagueFull extends LeagueBasic {
  seasonId:     number;
  seasonName:   string;
  startDate:    string;   // "YYYY-MM-DD"
  endDate:      string;   // "YYYY-MM-DD"
  primaryColor: string;   // Hex
  secondaryColor: string; // Hex
  officialSite: string;   // URL
  totalTeams:   number;
}

// GET /football_th/league.aspx?cmd=rule
// Returns league rules (round-robin format, stages, etc.)

export interface LeagueRule {
  leagueId:   number;
  seasonId:   number;
  info:       string; // HTML or plain text rules
}

// ── Live Data Plan ─────────────────────────────────────────

// GET /football_th/livescores.aspx (10 sec limit / 1 min recommended)

export interface LiveMatch {
  matchId:      number;
  leagueId:     number;
  leagueName:   string;
  homeTeamId:   number;
  homeTeamName: string;
  awayTeamId:   number;
  awayTeamName: string;
  homeScore:    number;
  awayScore:    number;
  homeHalfScore: number;
  awayHalfScore: number;
  state:        MatchState;
  matchTime:    UnixTimestamp;  // UTC kickoff time
  elapsed:      number;         // minutes elapsed (0 if not started)
  homeRed:      number;
  awayRed:      number;
  homeYellow:   number;
  awayYellow:   number;
  homeCorner:   number;
  awayCorner:   number;
  round:        string;
  hasAnimation: boolean;        // true if live animation is available
  animationMatchId?: number;    // use in animation iFrame URL
}

export interface LivescoresResponse {
  code:    number;
  message: string;
  results: LiveMatch[];
}

// GET /football_th/schedule/results.aspx (60 sec limit / 6 hr recommended)

export interface ScheduledMatch {
  matchId:      number;
  leagueId:     number;
  leagueName:   string;
  homeTeamId:   number;
  homeTeamName: string;
  awayTeamId:   number;
  awayTeamName: string;
  homeScore:    number | null; // null = future match
  awayScore:    number | null;
  state:        MatchState;
  matchTime:    UnixTimestamp;
  round:        string;
}

// GET /football_th/matchevent.aspx (60 sec limit)
// Events include: goals, bookings, substitutions

export enum EventType {
  Goal        = 1,
  OwnGoal     = 2,
  YellowCard  = 3,
  RedCard     = 4,
  YellowRed   = 5,
  Substitution = 6,
  Penalty     = 7,
  MissedPenalty = 8,
}

export interface MatchEvent {
  matchId:   number;
  type:      EventType;
  minute:    number;
  addedTime: number;          // stoppage time minutes
  teamId:    number;
  playerId:  number;
  playerName: string;
  relatedPlayerId?:   number; // e.g. player who was substituted
  relatedPlayerName?: string;
}

// GET /football_th/matchlineup.aspx

export interface Player {
  playerId:   number;
  playerName: string;
  shirtNumber: number;
  position:   string;  // "GK" | "DF" | "MF" | "FW"
  isStarting: boolean;
  isCaptain:  boolean;
  countryCode: CountryCode;
}

export interface MatchLineup {
  matchId:      number;
  homeTeamId:   number;
  awayTeamId:   number;
  homeLineup:   Player[];
  awayLineup:   Player[];
  homeSubs:     Player[];
  awaySubs:     Player[];
  homeFormation: string;  // e.g. "4-3-3"
  awayFormation: string;
  homeCoach:    string;
  awayCoach:    string;
}

// ── Profile (Live Data Plan) ────────────────────────────────
// GET /football_th/league.aspx   → LeagueFull (see above)
// GET /football_th/team.aspx

export interface Team {
  teamId:      number;
  teamName:    string;
  teamNameEn:  string;
  teamLogo:    string;  // URL
  countryId:   number;
  countryName: string;
  stadium:     string;
  capacity:    number;
  founded:     number;  // Year
  website:     string;
}

// GET /football_th/player.aspx

export interface PlayerProfile {
  playerId:    number;
  playerName:  string;
  playerNameEn: string;
  photo:       string;  // URL
  nationality: CountryCode;
  position:    string;
  dateOfBirth: string;  // "YYYY-MM-DD"
  height:      number;  // cm
  weight:      number;  // kg
  currentTeamId: number;
}

// ── Stats Plan ─────────────────────────────────────────────
// GET /football_th/league/table.aspx (doc: /doc/docs_id=43)

export interface StandingRow {
  rank:          number;
  teamId:        number;
  teamName:      string;
  played:        number;
  won:           number;
  drawn:         number;
  lost:          number;
  goalsFor:      number;
  goalsAgainst:  number;
  goalDifference: number;
  points:        number;
  form:          string[];  // last 5 results: "W" | "D" | "L"
  winRate:       number;    // percentage 0–100
  drawRate:      number;
  loseRate:      number;
}

export interface StandingsResponse {
  code:      number;
  leagueId:  number;
  seasonId:  number;
  results:   StandingRow[];
}

// GET /football_th/topscorer.aspx (doc: /doc/docs_id=39)

export interface TopScorer {
  rank:       number;
  playerId:   number;
  playerName: string;
  teamId:     number;
  teamName:   string;
  goals:      number;
  fieldGoals: number;
  penalties:  number;
  assists:    number;
  appearances: number;
}

// GET /football_th/fifaranking.aspx (doc: /doc/docs_id=47)

export interface FifaRankingEntry {
  rank:          number;
  previousRank:  number;
  teamId:        number;
  teamName:      string;
  countryCode:   CountryCode;
  points:        number;
  previousPoints: number;
  rankChange:    number;  // positive = up, negative = down
}

// ── Odds ───────────────────────────────────────────────────

export interface AsianHandicapOdds {
  matchId:     number;
  bookmaker:   string;
  home:        number;  // odds for home side
  handicap:    number;  // e.g. -0.5
  away:        number;
  updatedAt:   UnixTimestamp;
}

export interface EuropeanOdds {
  matchId:   number;
  bookmaker: string;
  home:      number;
  draw:      number;
  away:      number;
  updatedAt: UnixTimestamp;
}

// ── Live Animation ─────────────────────────────────────────
// (no API response type—it is an iFrame/WebView URL)

export interface AnimationEmbedParams {
  matchId:   number;
  accessKey: string;
}

export function buildAnimationUrl(params: AnimationEmbedParams): string {
  return `https://www.isportslive8.com/football/detail.html?matchId=${params.matchId}&accessKey=${params.accessKey}`;
}

優先度別のルールカテゴリ

優先度カテゴリプレフィックス影響
1認証とルーティングauth-CRITICAL
2レート制限とキャッシュlimit-CRITICAL
3ライブアニメーション – タイミングと方法anim-HIGH
4エンドポイント固有のベストプラクティスep-HIGH
5エラー処理とレジリエンスerror-HIGH
6データの正規化と型data-MEDIUM
7セキュリティsecurity-CRITICAL

ルール

1. 認証とルーティング(CRITICAL)
auth-env-key – API キーを環境変数に保存する

❌ 誤り:

const url = `http://www.thscore.info/football_th/livescores.aspx?api_key=abc123`;

✅ 正しい例:

const API_KEY = process.env.THSCORE_API_KEY;
if (!API_KEY) throw new Error("THSCORE_API_KEY is not set");
const url = `http://www.thscore.info/football_th/livescores.aspx?api_key=${API_KEY}`;
auth-asia-server – アジアに設置されたサーバーでは api2.thscore.info を使用する
// config.ts
export const THSCORE_HOST =
  process.env.THSCORE_REGION === "asia"
    ? "api2.thscore.info"
    : "www.thscore.info";

export function buildUrl(path: string, params: Record<string, string> = {}): string {
  const base = `http://${THSCORE_HOST}${path}`;
  const query = new URLSearchParams({
    api_key: process.env.THSCORE_API_KEY!,
    ...params,
  });
  return `${base}?${query.toString()}`;
}

2. レート制限とキャッシュ(CRITICAL)
エンドポイントハードリミット推奨
Livescores for Today10 秒/回1 分/回
Schedule & Results60 秒/回6 時間/回
League & Cup Profile (Basic)1 時間/回1 日/回
League & Cup Profile (Full)30 分/回1 日/回
Standings—1 時間/回
Top Scorers—1 日/回
FIFA Ranking—1 週間/回
limit-tiered-cache – エンドポイントの種類に適した TTL を使用する
// cache.service.ts (using Redis)
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

async function cachedFetch<T>(key: string, ttlSeconds: number, fetcher: () => Promise<T>): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached) as T;

  const data = await fetcher();
  await redis.setex(key, ttlSeconds, JSON.stringify(data));
  return data;
}

// Usage examples:
// Livescores — cache 60 seconds
const livescores = await cachedFetch("thscore:livescores", 60, fetchLivescores);

// League list — cache 24 hours
const leagues = await cachedFetch("thscore:leagues", 86400, fetchLeagues);

// Standings — cache 1 hour
const standings = await cachedFetch(`thscore:standings:${leagueId}`, 3600, () => fetchStandings(leagueId));
limit-background-sync – HTTP リクエストハンドラー内で API をポーリングしない
// BAD: polling inside a route handler
app.get("/api/livescores", async (req, res) => {
  const data = await fetch(buildUrl("/football_th/livescores.aspx")); // ← blocks users
  res.json(await data.json());
});

// GOOD: route handler reads from cache; a cron job populates it
app.get("/api/livescores", async (req, res) => {
  const cached = await redis.get("thscore:livescores");
  res.json(cached ? JSON.parse(cached) : { code: 503, message: "Data not ready yet" });
});

// cron job (every 60 seconds)
cron.schedule("* * * * *", async () => {
  const data = await fetchLivescores();
  await redis.setex("thscore:livescores", 90, JSON.stringify(data));
});

3. ライブアニメーション – タイミングと方法(HIGH)
anim-when – ライブアニメーションデータを利用できるタイミング

ライブアニメーションは、試合がライブ中で、次のいずれかのアクティブな状態にある場合にのみ利用できます。

import { MatchState } from "./types";

const ANIMATION_STATES: MatchState[] = [
  MatchState.FirstHalf,
  MatchState.HalfTime,
  MatchState.SecondHalf,
  MatchState.ExtraTimeFirst,
  MatchState.ExtraTimeHT,
  MatchState.ExtraTimeSecond,
  MatchState.PenaltyShootout,
];

export function isAnimationAvailable(match: LiveMatch): boolean {
  return ANIMATION_STATES.includes(match.state) && match.hasAnimation === true;
}

Livescores のレスポンスで hasAnimation: true を確認してください。予定されている試合や終了した試合では iFrame を埋め込まないでください。アニメーション URL は空白のページを返します。

anim-embed-html – ページにアニメーションを埋め込む(PC/H5 は iFrame を使用)

<div id="animation-container" style="display:none">
  <iframe
    id="match-animation"
    src=""
    width="100%"
    height="480"
    frameborder="0"
    allowfullscreen
    loading="lazy"
  ></iframe>
</div>
// match-detail.ts
import { buildAnimationUrl, isAnimationAvailable, LiveMatch } from "./types";

function renderAnimation(match: LiveMatch): void {
  const container = document.getElementById("animation-container")!;
  const iframe = document.getElementById("match-animation") as HTMLIFrameElement;

  if (!isAnimationAvailable(match)) {
    container.style.display = "none";
    return;
  }

  const url = buildAnimationUrl({
    matchId:   match.animationMatchId ?? match.matchId,
    accessKey: process.env.THSCORE_ANIMATION_ACCESS_KEY!,
  });

  iframe.src = url;
  container.style.display = "block";
}
anim-webview-mobile – WebView でアニメーションを読み込む(React Native)
// AnimationPlayer.tsx
import React from "react";
import { WebView } from "react-native-webview";
import { buildAnimationUrl } from "../types";

interface Props {
  matchId: number;
  accessKey: string;
}

export const AnimationPlayer: React.FC<Props> = ({ matchId, accessKey }) => {
  const url = buildAnimationUrl({ matchId, accessKey });
  return (
    <WebView
      source={{ uri: url }}
      style={{ flex: 1 }}
      allowsFullscreenVideo
      mediaPlaybackRequiresUserAction={false}
    />
  );
};
anim-domain-whitelist – コントロールパネルでドメインを登録する
  • iFrame で埋め込む前に、コンソールパネル → アニメーション → ドメインホワイトリストに移動します。
  • WebView を使用する APP プラットフォームでは、ホワイトリストは不要です。URL に直接アクセスできます。

4. エンドポイント固有のベストプラクティス(高)
ep-league-basic – リーグ&カップ情報(基本)

エンドポイント: GET /football_th/league/basic.aspx レート: ハード上限:1時間に1回 · 推奨:1日に1回 プラン: すべての有料サッカープラン(Stats、Live Data、Odds、Odds Pro、Betfair)

このエンドポイントが返す内容:

  • leagueId – ライブスコア、スケジュール、順位表、オッズのデータとの結合に使用します。
  • leagueName / leagueNameEn – タイ語と英語の表示名です。
  • countryId / countryName – 国別にリーグをグループ化します。
  • leagueLogo / countryLogo – CDN の画像 URL です。
  • type – 1 = リーグ、2 = カップ、3 = 国際大会です。
  • curSeasonId – 現在進行中のシーズンです。統計または順位表のエンドポイントを呼び出す際に使用します。
  • 140 か国以上、1,600 以上のリーグとカップを網羅しています。

✅ ベストプラクティス:

// league-service.ts
import { LeagueBasic, LeagueBasicResponse } from "./types";
import { buildUrl, cachedFetch } from "./utils";

/** Fetch and cache the full league catalogue for 24 hours. */
export async function getLeagues(): Promise<LeagueBasic[]> {
  return cachedFetch<LeagueBasic[]>("thscore:leagues:basic", 86400, async () => {
    const res = await fetch(buildUrl("/football_th/league/basic.aspx"));
    const json: LeagueBasicResponse = await res.json();
    if (json.code !== 0) throw new Error(`API error: ${json.message}`);
    return json.results;
  });
}

/** Group leagues by country for a country-selector UI */
export function groupByCountry(leagues: LeagueBasic[]): Record<string, LeagueBasic[]> {
  return leagues.reduce((acc, league) => {
    const key = league.countryNameEn;
    if (!acc[key]) acc[key] = [];
    acc[key].push(league);
    return acc;
  }, {} as Record<string, LeagueBasic[]>);
}

/** Filter for Cups only */
export function getCups(leagues: LeagueBasic[]): LeagueBasic[] {
  return leagues.filter(l => l.type === 2);
}

/** Find league by name (case-insensitive) */
export function findLeague(leagues: LeagueBasic[], name: string): LeagueBasic | undefined {
  const q = name.toLowerCase();
  return leagues.find(l => l.leagueNameEn.toLowerCase().includes(q));
}
ep-league-full – リーグ&カップ情報(全項目)

エンドポイント: GET /football_th/league.aspx レート: ハード上限:30分に1回 · 推奨:1日に1回 プラン: Live Data のみ

基本版に加えて、以下のフィールドを利用できます:

  • seasonId、seasonName – 進行中のシーズンの詳細(例:"2024-2025")
  • startDate、endDate – シーズンの期間
  • primaryColor、secondaryColor – UI のテーマ設定に使用するブランドカラー
  • totalTeams – 参加チーム数
  • officialSite – リーグの公式ウェブサイト

対応する機能: GET /football_th/league.aspx?cmd=rule
リーグのルールをプレーンテキスト/HTML で返します。リーグ情報ページに便利です。

ep-livescores – 本日のライブスコア

エンドポイント: GET /football_th/livescores.aspx レート: ハード上限:10秒に1回 · 推奨:1分に1回 プラン: Live Data

活用する主なレスポンスフィールド:

  • state → MatchState 列挙型にマッピングします
  • elapsed → 試合中の経過時間を表示します
  • homeScore、awayScore、homeHalfScore、awayHalfScore → スコアカードに使用します
  • homeCorner、awayCorner → コーナーキック数に使用します
  • homeRed、awayRed → レッドカードの表示に使用します
  • hasAnimation → アニメーション埋め込みボタンの表示可否を判定します
// livescores.service.ts
import { LivescoresResponse, LiveMatch, MatchState, MatchStateLabel } from "./types";
import { buildUrl, cachedFetch } from "./utils";

export async function getLivescores(): Promise<LiveMatch[]> {
  return cachedFetch<LiveMatch[]>("thscore:livescores", 60, async () => {
    const res = await fetch(buildUrl("/football_th/livescores.aspx"));
    const json: LivescoresResponse = await res.json();
    if (json.code !== 0) throw new Error(`[Livescores] API error ${json.code}: ${json.message}`);
    return json.results;
  });
}

/** Returns only currently in-play matches */
export function getInPlayMatches(matches: LiveMatch[]): LiveMatch[] {
  const inPlayStates = [
    MatchState.FirstHalf, MatchState.HalfTime,
    MatchState.SecondHalf, MatchState.ExtraTimeFirst,
    MatchState.ExtraTimeHT, MatchState.ExtraTimeSecond,
    MatchState.PenaltyShootout,
  ];
  return matches.filter(m => inPlayStates.includes(m.state));
}

/** Returns a display-friendly score string e.g. "2 - 1 (1 - 0)" */
export function formatScore(match: LiveMatch): string {
  return `${match.homeScore} - ${match.awayScore} (HT: ${match.homeHalfScore} - ${match.awayHalfScore})`;
}

/** Display-friendly status with elapsed minutes */
export function formatStatus(match: LiveMatch): string {
  const label = MatchStateLabel[match.state] ?? "Unknown";
  if (match.elapsed > 0 && match.state === MatchState.FirstHalf || match.state === MatchState.SecondHalf) {
    return `${label} ${match.elapsed}'`;
  }
  return label;
}
ep-standings – リーグ&カップの順位表

エンドポイント: GET /football_th/league/table.aspx(ドキュメント #43 を参照) プラン: Stats

表示するレスポンスフィールド:

  • rank – 順位表での順位
  • played、won、drawn、lost – 試合成績
  • goalsFor、goalsAgainst、goalDifference – 得点に関する統計
  • points – 勝ち点の合計
  • form – 直近5試合の結果の配列 ["W","W","D","L","W"]
  • winRate、drawRate、loseRate – 割合の統計(分析タブに便利です)
// standings.service.ts
import { StandingRow, StandingsResponse } from "./types";
import { buildUrl, cachedFetch } from "./utils";

export async function getStandings(leagueId: number, seasonId: number): Promise<StandingRow[]> {
  const key = `thscore:standings:${leagueId}:${seasonId}`;
  return cachedFetch(key, 3600, async () => {
    const res = await fetch(buildUrl("/football_th/league/table.aspx", {
      leagueId: String(leagueId),
      seasonId: String(seasonId),
    }));
    const json: StandingsResponse = await res.json();
    if (json.code !== 0) throw new Error(`[Standings] API error: ${json.code}`);
    return json.results;
  });
}

/** Colour-code a form result for UI rendering */
export function formBadgeColor(result: string): string {
  return result === "W" ? "green" : result === "D" ? "gray" : "red";
}

5. エラー処理と回復性(高)
error-retry-backoff – 一時的なエラーに対し、指数バックオフで再試行する
// utils/http.ts
export async function fetchWithRetry(url: string, maxRetries = 3): Promise<Response> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 8000); // 8s timeout

      const res = await fetch(url, { signal: controller.signal });
      clearTimeout(timeout);

      if (res.ok) return res;
      if (res.status >= 500) throw new Error(`Server error ${res.status}`);
      throw new Error(`Client error ${res.status}: ${res.statusText}`);
    } catch (err) {
      if (attempt === maxRetries - 1) throw err;
      const delay = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s
      console.warn(`[THScore] Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error("Unreachable");
}
error-graceful-degrade – API が利用できない場合は古いデータを表示する
export async function getLivescoresSafe(): Promise<LiveMatch[]> {
  try {
    return await getLivescores();
  } catch (err) {
    console.error("[THScore] Livescores unavailable, serving stale cache:", err);
    const stale = await redis.get("thscore:livescores:stale");
    return stale ? JSON.parse(stale) : [];
  }
}

// On successful fetch, always persist a "stale" backup
async function fetchAndPersist(): Promise<void> {
  const data = await getLivescores();
  await redis.set("thscore:livescores:stale", JSON.stringify(data)); // no TTL = forever
  await redis.setex("thscore:livescores", 60, JSON.stringify(data));
}

6. データの正規化と型(中)
data-map-on-ingestion – API のデータ構造を内部型にすぐ変換する

THScore の生の JSON を UI やデータベースに直接渡してはいけません。必ず型付きの変換処理を介してマッピングしてください。

// mappers.ts
import { LiveMatch, MatchState } from "./types";

/** Raw shape from API (untrusted, not typed) */
interface RawMatch {
  matchId: number;
  state: number;
  homeName: string;
  awayName: string;
  homeScore: number;
  awayScore: number;
  hHalf: number;
  aHalf: number;
  // ... more raw fields
}

export function mapRawMatch(raw: RawMatch): LiveMatch {
  return {
    matchId:       raw.matchId,
    homeTeamName:  raw.homeName,
    awayTeamName:  raw.awayName,
    homeScore:     raw.homeScore ?? 0,
    awayScore:     raw.awayScore ?? 0,
    homeHalfScore: raw.hHalf ?? 0,
    awayHalfScore: raw.aHalf ?? 0,
    state:         raw.state as MatchState,
    hasAnimation:  Boolean(raw.animId),
    animationMatchId: raw.animId ?? undefined,
    // ... map remaining fields
  } as LiveMatch;
}

7. セキュリティ(重大)
security-proxy-all – 必ずバックエンド経由でプロキシする

THScore は CORS に対応していません。ブラウザーから thscore.info に直接リクエストすると、すべてブロックされます。API キーはネットワークログに露出したままになります。

✅ アーキテクチャ:

Browser / App
     │
     ▼
Your Backend (Node / PHP / Python)   ← holds THSCORE_API_KEY
     │
     ▼
api2.thscore.info / www.thscore.info
// routes/football.ts (Express example)
router.get("/livescores", async (req, res) => {
  const data = await getLivescoresSafe();
  res.json(data); // ← never expose raw API key to client
});

公式ドキュメントのリファレンス

トピックURL
はじめにhttps://www.thscore.info/doc/docs.shtml
Football API ホームhttps://www.thscore.info/doc/docs_id=42.shtml
リーグ&カップのプロフィール(基本)/football_th/league/basic.aspx
リーグ&カップのプロフィール(完全版)/football_th/league.aspx
本日のライブスコアdoc/docs_id=20 · /football_th/livescores.aspx
日程&結果doc/docs_id=21 · /football_th/schedule/results.aspx
試合のイベント&統計doc/docs_id=15
ラインアップ&負傷情報doc/docs_id=17
リーグ&カップの順位表doc/docs_id=43
得点ランキングdoc/docs_id=39
FIFA ランキングdoc/docs_id=47
ライブアニメーション設定doc/docs_id=242
アニメーションライブ URL のデモhttps://www.isportslive8.com
全サッカーリーグ一覧https://www.thscore.info/LeagueTh.aspx

コメント