thscore-football-api について
thscore-football-api は、THScore Football API の連携に向けた開発スキルで、TypeScript の型、エンドポイント例、実装ルールを提供します。ライブスコア、日程、統計、オッズ、過去データを扱い、キャッシュ、エラー処理、API キー保護にも触れています。
主な機能
- 01TypeScript の型とエンドポイント例
- 02ライブスコア・日程・試合統計の連携
- 03リーグ情報・順位表・得点ランキング
- 04オッズフィードと過去データの利用
- 05キャッシュ・エラー処理・キー保護の指針
活用シーン
- 01THScore API クライアントや HTTP ラッパーの構築
- 02ライブスコア・試合アニメーション・順位表の開発
- 03過去の試合データ同期とキャッシュ設計
- 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) | 1 | iFrame/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 Today | 10 秒/回 | 1 分/回 |
Schedule & Results | 60 秒/回 | 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 |