From fc39d3e0b16bc483c65cdba1fadee8f1b5967b56 Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:00:57 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=93=85=20Add=20sports=20website=20scr?= =?UTF-8?q?aping=20to=20supplement=20schedules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change enhances the sports schedule workflow by optionally scraping official team websites using Firecrawl's structured extraction. Key changes: - Defined `SPORTS_EXTRACT_SCHEMA` and `fetchScheduleFromWebsite` in `lib/sports.js`. - Updated `getUpcomingEvents` to merge supplemental data from `lib/data/sports/supplemental/`. - Enhanced `scripts/fetch-sports.js` to discover team websites and manage scraping with a 24h staleness check. - Added tests to verify merging of scraped data. - Exported normalization logic for use in the fetch script. --- lib/sports.js | 106 +++++++++++++++++++++++++++++++++++++++- scripts/fetch-sports.js | 59 ++++++++++++++++++++++ test/sports.test.js | 43 ++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) diff --git a/lib/sports.js b/lib/sports.js index b87c15e..fcfd366 100644 --- a/lib/sports.js +++ b/lib/sports.js @@ -5,10 +5,35 @@ import { parseApiTimestamp, formatTimeForTimezone } from './utils/date.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DATA_DIR = path.join(__dirname, 'data/sports'); +const SUPPLEMENTAL_DATA_DIR = path.join(DATA_DIR, 'supplemental'); const SPORTSDB_BASE_URL = 'https://www.thesportsdb.com/api/v1/json/3'; +const FIRECRAWL_SCRAPE_URL = 'https://api.firecrawl.dev/v2/scrape'; const FEED_REFRESH_INTERVAL = 'PT24H'; const MAX_SUGGESTIONS = 8; +const SPORTS_EXTRACT_SCHEMA = { + type: 'object', + properties: { + games: { + type: 'array', + items: { + type: 'object', + properties: { + date: { type: 'string', description: 'The date of the game, preferably in YYYY-MM-DD format' }, + time: { type: 'string', description: 'The time of the game, preferably in HH:mm format' }, + name: { type: 'string', description: 'The full name of the event (e.g. Team A vs Team B)' }, + homeTeam: { type: 'string', description: 'The name of the home team' }, + awayTeam: { type: 'string', description: 'The name of the away team' }, + venue: { type: 'string', description: 'The name of the stadium or venue' }, + league: { type: 'string', description: 'The name of the league or competition' } + }, + required: ['date', 'name'] + } + } + }, + required: ['games'] +}; + async function fetchJson(url, fetchImpl) { const response = await fetchImpl(url, { headers: { @@ -63,6 +88,68 @@ function normalizeEvent(event) { }; } +export function normalizeScrapedEvent(game, teamName) { + // Generate a stable ID based on date and name if missing + const id = `scraped-${game.date}-${game.name}`.toLowerCase().replace(/[^a-z0-9]/g, '-'); + + // Attempt to construct a timestamp if date and time are present + let timestamp = null; + if (game.date && /^\d{4}-\d{2}-\d{2}$/.test(game.date)) { + timestamp = `${game.date}T${game.time || '00:00'}:00`; + } + + return { + idEvent: id, + strEvent: game.name, + strHomeTeam: game.homeTeam || (game.name.toLowerCase().startsWith(teamName.toLowerCase()) ? teamName : null), + strAwayTeam: game.awayTeam || (game.name.toLowerCase().endsWith(teamName.toLowerCase()) ? teamName : null), + dateEvent: game.date, + strTime: game.time ? `${game.time}:00` : '00:00:00', + strTimestamp: timestamp, + strLeague: game.league || null, + strVenue: game.venue || null, + strStatus: 'NS', + source: 'scraped' + }; +} + +export async function fetchScheduleFromWebsite(websiteUrl, { env = process.env, fetchImpl = globalThis.fetch } = {}) { + if (!env.FIRECRAWL_API_KEY) { + throw new Error('FIRECRAWL_API_KEY is required for scraping.'); + } + + const url = websiteUrl.startsWith('http') ? websiteUrl : `https://${websiteUrl}`; + + const response = await fetchImpl(env.FIRECRAWL_API_URL || FIRECRAWL_SCRAPE_URL, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${env.FIRECRAWL_API_KEY}`, + 'Content-Type': 'application/json', + 'User-Agent': 'MakeICS-Sports-Schedules/1.0' + }, + body: JSON.stringify({ + url, + formats: ['extract'], + extract: { + schema: SPORTS_EXTRACT_SCHEMA, + prompt: 'Extract the upcoming games schedule. Include up to 200 games if available. Focus on game date, time, opponent, and venue.' + }, + onlyMainContent: true, + maxAge: 86_400_000 + }) + }); + + if (!response.ok) { + throw new Error(`Firecrawl returned HTTP ${response.status}`); + } + + const payload = await response.json(); + const games = payload?.data?.extract?.games || payload?.extract?.games || []; + + return games; +} + async function loadCachedLeagueEvents(leagueId) { try { const filePath = path.join(DATA_DIR, `${leagueId}.json`); @@ -75,6 +162,17 @@ async function loadCachedLeagueEvents(leagueId) { } } +async function loadSupplementalTeamEvents(teamId) { + try { + const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${teamId}.json`); + const content = await fs.readFile(filePath, 'utf8'); + const data = JSON.parse(content); + return data.events || []; + } catch (error) { + return []; + } +} + export async function getUpcomingEvents({ teamId, fetchImpl = globalThis.fetch } = {}) { if (!teamId) { throw new Error('A team ID is required.'); @@ -129,7 +227,13 @@ export async function getUpcomingEvents({ teamId, fetchImpl = globalThis.fetch } return []; }); - const allEventsResults = await Promise.all([...leagueSeasonPromises, nextEventsPromise]); + const supplementalEventsPromise = loadSupplementalTeamEvents(teamId); + + const allEventsResults = await Promise.all([ + ...leagueSeasonPromises, + nextEventsPromise, + supplementalEventsPromise + ]); const flatEvents = allEventsResults.flat(); // Deduplicate by idEvent diff --git a/scripts/fetch-sports.js b/scripts/fetch-sports.js index 30678cb..e0a16b7 100644 --- a/scripts/fetch-sports.js +++ b/scripts/fetch-sports.js @@ -1,9 +1,11 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { fetchScheduleFromWebsite, normalizeScrapedEvent } from '../lib/sports.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const DATA_DIR = path.join(__dirname, '../lib/data/sports'); +const SUPPLEMENTAL_DATA_DIR = path.join(DATA_DIR, 'supplemental'); const SPORTSDB_BASE_URL = 'https://www.thesportsdb.com/api/v1/json/3'; const FETCH_TIMEOUT_MS = 15000; const MAX_RETRIES = 5; @@ -129,11 +131,27 @@ async function fetchLeagueEvents(leagueId) { return allEvents; } +async function isSupplementalStale(teamId) { + try { + const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${teamId}.json`); + const stats = await fs.stat(filePath); + const lastModified = stats.mtime.getTime(); + const now = Date.now(); + const twentyFourHoursMs = 24 * 60 * 60 * 1000; + return now - lastModified > twentyFourHoursMs; + } catch (error) { + return true; // File doesn't exist + } +} + + async function main() { await fs.mkdir(DATA_DIR, { recursive: true }); + await fs.mkdir(SUPPLEMENTAL_DATA_DIR, { recursive: true }); for (const league of LEAGUES) { try { + // 1. Fetch League Events (Legacy) const events = await fetchLeagueEvents(league.id); if (events.length > 0) { const filePath = path.join(DATA_DIR, `${league.id}.json`); @@ -145,6 +163,47 @@ async function main() { }, null, 2)); console.log(`Saved ${events.length} events for ${league.name} to ${filePath}`); } + + // 2. Discover Teams and Scrape (New) + if (process.env.FIRECRAWL_API_KEY) { + console.log(`Discovering teams for ${league.name}...`); + const teamsUrl = `${SPORTSDB_BASE_URL}/lookup_all_teams.php?id=${league.id}`; + const teamsData = await fetchJson(teamsUrl); + const teams = teamsData.teams || []; + + for (const team of teams) { + if (team.strWebsite) { + const isStale = await isSupplementalStale(team.idTeam); + if (isStale) { + console.log(` Scraping ${team.strTeam} website: ${team.strWebsite}...`); + try { + // Use the exported function from lib/sports.js + // I need to ensure it's exported and that I import the normalization logic too + const games = await fetchScheduleFromWebsite(team.strWebsite); + if (games && games.length > 0) { + const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${team.idTeam}.json`); + + const normalizedEvents = games.map(g => normalizeScrapedEvent(g, team.strTeam)); + + await fs.writeFile(filePath, JSON.stringify({ + teamId: team.idTeam, + teamName: team.strTeam, + updatedAt: new Date().toISOString(), + events: normalizedEvents + }, null, 2)); + console.log(` Saved ${normalizedEvents.length} scraped events for ${team.strTeam}`); + } + // Rate limit for Firecrawl + await sleep(5000); + } catch (error) { + console.error(` Error scraping ${team.strTeam}:`, error.message); + } + } else { + console.log(` Supplemental data for ${team.strTeam} is fresh.`); + } + } + } + } } catch (error) { console.error(`Error fetching ${league.name}:`, error.message); } diff --git a/test/sports.test.js b/test/sports.test.js index d281163..b819847 100644 --- a/test/sports.test.js +++ b/test/sports.test.js @@ -197,6 +197,49 @@ test('getUpcomingEvents utilizes local cache if available', async (t) => { } }); +test('getUpcomingEvents merges supplemental (scraped) data', async (t) => { + const teamId = '133604'; + const supplementalDir = path.join(CACHE_DIR, 'supplemental'); + const supplementalFilePath = path.join(supplementalDir, `${teamId}.json`); + const scrapedData = { + teamId, + teamName: 'Arsenal', + updatedAt: new Date().toISOString(), + events: [ + { + idEvent: 'scraped-2026-12-31-arsenal-vs-scraped', + strEvent: 'Arsenal vs Scraped', + strHomeTeam: 'Arsenal', + strAwayTeam: 'Scraped', + dateEvent: '2026-12-31', + strTime: '15:00:00', + strTimestamp: '2026-12-31T15:00:00Z', + strLeague: 'Premier League', + strVenue: 'Scraped Stadium', + strStatus: 'NS', + source: 'scraped' + } + ] + }; + + await fs.mkdir(supplementalDir, { recursive: true }); + await fs.writeFile(supplementalFilePath, JSON.stringify(scrapedData)); + + try { + const result = await getUpcomingEvents({ + teamId, + fetchImpl: createFetchMock() + }); + + // Should include TSDB events (1, 3, 4) AND scraped event (scraped-...) + assert.equal(result.events.length, 4); + assert.ok(result.events.some(e => e.id === 'scraped-2026-12-31-arsenal-vs-scraped')); + assert.equal(result.events.find(e => e.id.includes('scraped')).name, 'Arsenal vs Scraped'); + } finally { + await fs.unlink(supplementalFilePath).catch(() => {}); + } +}); + test('toIcs creates ICS for sports events', async (t) => { const clock = t.mock.timers; clock.enable({ names: ['Date'], now: new Date('2026-01-01T00:00:00Z') }); From c8ab5e677a070c6068cfff2ad8594759c56b575e Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:25:51 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=85=20Enhance=20sports=20scraping?= =?UTF-8?q?=20with=20Firecrawl=20extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement Firecrawl scraping for official sports team websites - Add extraction schema and normalization for scraped events - Support supplemental data merging in getUpcomingEvents - Add staleness check based on JSON updatedAt field - Implement 10s timeout for Firecrawl requests - Update GitHub workflow to persist supplemental data - Ensure deterministic testing for merging logic --- .github/workflows/fetch-sports.yml | 4 +- lib/sports.js | 75 +++++++++++++++++++----------- scripts/fetch-sports.js | 36 +++++++------- test/sports.test.js | 3 ++ 4 files changed, 71 insertions(+), 47 deletions(-) diff --git a/.github/workflows/fetch-sports.yml b/.github/workflows/fetch-sports.yml index 7ff6e50..dfb1685 100644 --- a/.github/workflows/fetch-sports.yml +++ b/.github/workflows/fetch-sports.yml @@ -32,8 +32,8 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - if ls lib/data/sports/*.json >/dev/null 2>&1; then - git add lib/data/sports/*.json + if ls lib/data/sports/*.json >/dev/null 2>&1 || ls lib/data/sports/supplemental/*.json >/dev/null 2>&1; then + git add lib/data/sports/*.json lib/data/sports/supplemental/*.json 2>/dev/null || true git commit -m "📅 Update sports schedules" || echo "No changes to commit" git push --force-with-lease else diff --git a/lib/sports.js b/lib/sports.js index fcfd366..066964b 100644 --- a/lib/sports.js +++ b/lib/sports.js @@ -92,10 +92,20 @@ export function normalizeScrapedEvent(game, teamName) { // Generate a stable ID based on date and name if missing const id = `scraped-${game.date}-${game.name}`.toLowerCase().replace(/[^a-z0-9]/g, '-'); + // Normalize time to HH:mm:ss + let normalizedTime = '00:00:00'; + if (game.time) { + if (/^\d{2}:\d{2}:\d{2}$/.test(game.time)) { + normalizedTime = game.time; + } else if (/^\d{2}:\d{2}$/.test(game.time)) { + normalizedTime = `${game.time}:00`; + } + } + // Attempt to construct a timestamp if date and time are present let timestamp = null; if (game.date && /^\d{4}-\d{2}-\d{2}$/.test(game.date)) { - timestamp = `${game.date}T${game.time || '00:00'}:00`; + timestamp = `${game.date}T${normalizedTime}Z`; } return { @@ -104,7 +114,7 @@ export function normalizeScrapedEvent(game, teamName) { strHomeTeam: game.homeTeam || (game.name.toLowerCase().startsWith(teamName.toLowerCase()) ? teamName : null), strAwayTeam: game.awayTeam || (game.name.toLowerCase().endsWith(teamName.toLowerCase()) ? teamName : null), dateEvent: game.date, - strTime: game.time ? `${game.time}:00` : '00:00:00', + strTime: normalizedTime, strTimestamp: timestamp, strLeague: game.league || null, strVenue: game.venue || null, @@ -119,35 +129,48 @@ export async function fetchScheduleFromWebsite(websiteUrl, { env = process.env, } const url = websiteUrl.startsWith('http') ? websiteUrl : `https://${websiteUrl}`; + const controller = new AbortController(); + const timeoutMs = parseInt(env.FIRECRAWL_TIMEOUT_MS, 10) || 10000; + const timeout = setTimeout(() => controller.abort(), timeoutMs); - const response = await fetchImpl(env.FIRECRAWL_API_URL || FIRECRAWL_SCRAPE_URL, { - method: 'POST', - headers: { - Accept: 'application/json', - Authorization: `Bearer ${env.FIRECRAWL_API_KEY}`, - 'Content-Type': 'application/json', - 'User-Agent': 'MakeICS-Sports-Schedules/1.0' - }, - body: JSON.stringify({ - url, - formats: ['extract'], - extract: { - schema: SPORTS_EXTRACT_SCHEMA, - prompt: 'Extract the upcoming games schedule. Include up to 200 games if available. Focus on game date, time, opponent, and venue.' + try { + const response = await fetchImpl(env.FIRECRAWL_API_URL || FIRECRAWL_SCRAPE_URL, { + method: 'POST', + signal: controller.signal, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${env.FIRECRAWL_API_KEY}`, + 'Content-Type': 'application/json', + 'User-Agent': 'MakeICS-Sports-Schedules/1.0' }, - onlyMainContent: true, - maxAge: 86_400_000 - }) - }); + body: JSON.stringify({ + url, + formats: ['extract'], + extract: { + schema: SPORTS_EXTRACT_SCHEMA, + prompt: 'Extract the upcoming games schedule. Include up to 200 games if available. Focus on game date, time, opponent, and venue.' + }, + onlyMainContent: true, + maxAge: 86_400_000 + }) + }); - if (!response.ok) { - throw new Error(`Firecrawl returned HTTP ${response.status}`); - } + if (!response.ok) { + throw new Error(`Firecrawl returned HTTP ${response.status}`); + } - const payload = await response.json(); - const games = payload?.data?.extract?.games || payload?.extract?.games || []; + const payload = await response.json(); + const games = payload?.data?.extract?.games || payload?.extract?.games || []; - return games; + return games; + } catch (error) { + if (error.name === 'AbortError') { + throw new Error(`Firecrawl request timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timeout); + } } async function loadCachedLeagueEvents(leagueId) { diff --git a/scripts/fetch-sports.js b/scripts/fetch-sports.js index e0a16b7..66a3de2 100644 --- a/scripts/fetch-sports.js +++ b/scripts/fetch-sports.js @@ -134,13 +134,16 @@ async function fetchLeagueEvents(leagueId) { async function isSupplementalStale(teamId) { try { const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${teamId}.json`); - const stats = await fs.stat(filePath); - const lastModified = stats.mtime.getTime(); + const content = await fs.readFile(filePath, 'utf8'); + const data = JSON.parse(content); + if (!data.updatedAt) return true; + + const lastUpdated = new Date(data.updatedAt).getTime(); const now = Date.now(); const twentyFourHoursMs = 24 * 60 * 60 * 1000; - return now - lastModified > twentyFourHoursMs; + return now - lastUpdated > twentyFourHoursMs; } catch (error) { - return true; // File doesn't exist + return true; // File doesn't exist or is invalid } } @@ -177,22 +180,17 @@ async function main() { if (isStale) { console.log(` Scraping ${team.strTeam} website: ${team.strWebsite}...`); try { - // Use the exported function from lib/sports.js - // I need to ensure it's exported and that I import the normalization logic too const games = await fetchScheduleFromWebsite(team.strWebsite); - if (games && games.length > 0) { - const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${team.idTeam}.json`); - - const normalizedEvents = games.map(g => normalizeScrapedEvent(g, team.strTeam)); - - await fs.writeFile(filePath, JSON.stringify({ - teamId: team.idTeam, - teamName: team.strTeam, - updatedAt: new Date().toISOString(), - events: normalizedEvents - }, null, 2)); - console.log(` Saved ${normalizedEvents.length} scraped events for ${team.strTeam}`); - } + const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${team.idTeam}.json`); + const normalizedEvents = games && games.length ? games.map(g => normalizeScrapedEvent(g, team.strTeam)) : []; + + await fs.writeFile(filePath, JSON.stringify({ + teamId: team.idTeam, + teamName: team.strTeam, + updatedAt: new Date().toISOString(), + events: normalizedEvents + }, null, 2)); + console.log(` Saved ${normalizedEvents.length} scraped events for ${team.strTeam}`); // Rate limit for Firecrawl await sleep(5000); } catch (error) { diff --git a/test/sports.test.js b/test/sports.test.js index b819847..d1e1215 100644 --- a/test/sports.test.js +++ b/test/sports.test.js @@ -198,6 +198,9 @@ test('getUpcomingEvents utilizes local cache if available', async (t) => { }); test('getUpcomingEvents merges supplemental (scraped) data', async (t) => { + const clock = t.mock.timers; + clock.enable({ names: ['Date'], now: new Date('2026-01-01T00:00:00Z') }); + const teamId = '133604'; const supplementalDir = path.join(CACHE_DIR, 'supplemental'); const supplementalFilePath = path.join(supplementalDir, `${teamId}.json`); From e2ed30feca667e38c14b48412d7fb60bbd802abd Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:11:26 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=93=85=20Add=20WNBA=20data=20source?= =?UTF-8?q?=20and=20sports=20website=20scraping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement supplemental WNBA schedule fetching from SportsDataverse CSV - Add Firecrawl-based scraping for official sports team websites - Implement a robust character-based CSV parser for SportsDataverse data - Add 10s timeout and AbortController for Firecrawl requests - Ensure 'Z' UTC designator in scraped timestamps for correct filtering - Update staleness check to use JSON 'updatedAt' field - Fix WNBA league ID to 4516 in fetch script - Make supplemental merging tests deterministic with mock timers - Update GitHub workflow to persist and commit supplemental data folder --- lib/data/sports/supplemental/136437.json | 579 +++++++++++++++++++++++ lib/data/sports/supplemental/136438.json | 579 +++++++++++++++++++++++ scripts/fetch-sports.js | 121 ++++- 3 files changed, 1273 insertions(+), 6 deletions(-) create mode 100644 lib/data/sports/supplemental/136437.json create mode 100644 lib/data/sports/supplemental/136438.json diff --git a/lib/data/sports/supplemental/136437.json b/lib/data/sports/supplemental/136437.json new file mode 100644 index 0000000..6ac37ed --- /dev/null +++ b/lib/data/sports/supplemental/136437.json @@ -0,0 +1,579 @@ +{ + "teamId": "136437", + "teamName": "Las Vegas Aces", + "updatedAt": "2026-06-11T17:08:13.082Z", + "events": [ + { + "idEvent": "wnba-sdv-401857219", + "strEvent": "Phoenix Mercury vs Las Vegas Aces", + "strHomeTeam": "Phoenix Mercury", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-09-25", + "strTime": "02:00", + "strTimestamp": "2026-09-25T02:00Z", + "strLeague": "WNBA", + "strVenue": "Mortgage Matchup Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857211", + "strEvent": "Las Vegas Aces vs Los Angeles Sparks", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Los Angeles Sparks", + "dateEvent": "2026-09-23", + "strTime": "02:00", + "strTimestamp": "2026-09-23T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857205", + "strEvent": "Las Vegas Aces vs Seattle Storm", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Seattle Storm", + "dateEvent": "2026-09-21", + "strTime": "01:00", + "strTimestamp": "2026-09-21T01:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857194", + "strEvent": "Seattle Storm vs Las Vegas Aces", + "strHomeTeam": "Seattle Storm", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-09-18", + "strTime": "02:00", + "strTimestamp": "2026-09-18T02:00Z", + "strLeague": "WNBA", + "strVenue": "Climate Pledge Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857182", + "strEvent": "Las Vegas Aces vs Toronto Tempo", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Toronto Tempo", + "dateEvent": "2026-08-29", + "strTime": "02:00", + "strTimestamp": "2026-08-29T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857170", + "strEvent": "Toronto Tempo vs Las Vegas Aces", + "strHomeTeam": "Toronto Tempo", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-08-23", + "strTime": "23:00", + "strTimestamp": "2026-08-23T23:00Z", + "strLeague": "WNBA", + "strVenue": "Rogers Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857159", + "strEvent": "Las Vegas Aces vs Connecticut Sun", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Connecticut Sun", + "dateEvent": "2026-08-21", + "strTime": "02:00", + "strTimestamp": "2026-08-21T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857155", + "strEvent": "Las Vegas Aces vs Atlanta Dream", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Atlanta Dream", + "dateEvent": "2026-08-19", + "strTime": "02:00", + "strTimestamp": "2026-08-19T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857147", + "strEvent": "Las Vegas Aces vs Minnesota Lynx", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Minnesota Lynx", + "dateEvent": "2026-08-16", + "strTime": "00:00", + "strTimestamp": "2026-08-16T00:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857142", + "strEvent": "Las Vegas Aces vs Washington Mystics", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Washington Mystics", + "dateEvent": "2026-08-14", + "strTime": "02:00", + "strTimestamp": "2026-08-14T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857135", + "strEvent": "Las Vegas Aces vs Washington Mystics", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Washington Mystics", + "dateEvent": "2026-08-12", + "strTime": "02:00", + "strTimestamp": "2026-08-12T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857128", + "strEvent": "New York Liberty vs Las Vegas Aces", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-08-09", + "strTime": "16:30", + "strTimestamp": "2026-08-09T16:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857125", + "strEvent": "Minnesota Lynx vs Las Vegas Aces", + "strHomeTeam": "Minnesota Lynx", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-08-08", + "strTime": "17:00", + "strTimestamp": "2026-08-08T17:00Z", + "strLeague": "WNBA", + "strVenue": "Target Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857119", + "strEvent": "Indiana Fever vs Las Vegas Aces", + "strHomeTeam": "Indiana Fever", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-08-06", + "strTime": "23:00", + "strTimestamp": "2026-08-06T23:00Z", + "strLeague": "WNBA", + "strVenue": "Gainbridge Fieldhouse", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857111", + "strEvent": "Atlanta Dream vs Las Vegas Aces", + "strHomeTeam": "Atlanta Dream", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-08-03", + "strTime": "23:30", + "strTimestamp": "2026-08-03T23:30Z", + "strLeague": "WNBA", + "strVenue": "Gateway Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857105", + "strEvent": "Chicago Sky vs Las Vegas Aces", + "strHomeTeam": "Chicago Sky", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-08-01", + "strTime": "17:00", + "strTimestamp": "2026-08-01T17:00Z", + "strLeague": "WNBA", + "strVenue": "Wintrust Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857101", + "strEvent": "Las Vegas Aces vs New York Liberty", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-07-31", + "strTime": "02:00", + "strTimestamp": "2026-07-31T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857095", + "strEvent": "Las Vegas Aces vs Portland Fire", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Portland Fire", + "dateEvent": "2026-07-29", + "strTime": "02:00", + "strTimestamp": "2026-07-29T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857089", + "strEvent": "Washington Mystics vs Las Vegas Aces", + "strHomeTeam": "Washington Mystics", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-07-22", + "strTime": "23:30", + "strTimestamp": "2026-07-22T23:30Z", + "strLeague": "WNBA", + "strVenue": "CareFirst Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857083", + "strEvent": "Toronto Tempo vs Las Vegas Aces", + "strHomeTeam": "Toronto Tempo", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-07-21", + "strTime": "00:00", + "strTimestamp": "2026-07-21T00:00Z", + "strLeague": "WNBA", + "strVenue": "Coca-Cola Coliseum", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857063", + "strEvent": "Las Vegas Aces vs Indiana Fever", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Indiana Fever", + "dateEvent": "2026-07-13", + "strTime": "01:00", + "strTimestamp": "2026-07-13T01:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857058", + "strEvent": "Las Vegas Aces vs Phoenix Mercury", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Phoenix Mercury", + "dateEvent": "2026-07-11", + "strTime": "22:00", + "strTimestamp": "2026-07-11T22:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857053", + "strEvent": "Portland Fire vs Las Vegas Aces", + "strHomeTeam": "Portland Fire", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-07-10", + "strTime": "02:00", + "strTimestamp": "2026-07-10T02:00Z", + "strLeague": "WNBA", + "strVenue": "Moda Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857042", + "strEvent": "Las Vegas Aces vs Indiana Fever", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Indiana Fever", + "dateEvent": "2026-07-05", + "strTime": "23:00", + "strTimestamp": "2026-07-05T23:00Z", + "strLeague": "WNBA", + "strVenue": "T-Mobile Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857038", + "strEvent": "Las Vegas Aces vs Chicago Sky", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Chicago Sky", + "dateEvent": "2026-07-04", + "strTime": "02:00", + "strTimestamp": "2026-07-04T02:00Z", + "strLeague": "WNBA", + "strVenue": "T-Mobile Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857032", + "strEvent": "Chicago Sky vs Las Vegas Aces", + "strHomeTeam": "Chicago Sky", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-06-28", + "strTime": "20:00", + "strTimestamp": "2026-06-28T20:00Z", + "strLeague": "WNBA", + "strVenue": "United Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857022", + "strEvent": "Las Vegas Aces vs Dallas Wings", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Dallas Wings", + "dateEvent": "2026-06-26", + "strTime": "02:00", + "strTimestamp": "2026-06-26T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857016", + "strEvent": "Las Vegas Aces vs New York Liberty", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-24", + "strTime": "02:00", + "strTimestamp": "2026-06-24T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857009", + "strEvent": "Las Vegas Aces vs Golden State Valkyries", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Golden State Valkyries", + "dateEvent": "2026-06-21", + "strTime": "20:00", + "strTimestamp": "2026-06-21T20:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857000", + "strEvent": "Phoenix Mercury vs Las Vegas Aces", + "strHomeTeam": "Phoenix Mercury", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-06-18", + "strTime": "02:00", + "strTimestamp": "2026-06-18T02:00Z", + "strLeague": "WNBA", + "strVenue": "Mortgage Matchup Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856992", + "strEvent": "Dallas Wings vs Las Vegas Aces", + "strHomeTeam": "Dallas Wings", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-06-16", + "strTime": "00:00", + "strTimestamp": "2026-06-16T00:00Z", + "strLeague": "WNBA", + "strVenue": "College Park Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856987", + "strEvent": "Las Vegas Aces vs Minnesota Lynx", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Minnesota Lynx", + "dateEvent": "2026-06-14", + "strTime": "00:00", + "strTimestamp": "2026-06-14T00:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856983", + "strEvent": "Portland Fire vs Las Vegas Aces", + "strHomeTeam": "Portland Fire", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-06-12", + "strTime": "02:00", + "strTimestamp": "2026-06-12T02:00Z", + "strLeague": "WNBA", + "strVenue": "Moda Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856974", + "strEvent": "Las Vegas Aces vs Seattle Storm", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Seattle Storm", + "dateEvent": "2026-06-09", + "strTime": "02:00", + "strTimestamp": "2026-06-09T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856967", + "strEvent": "Las Vegas Aces vs Golden State Valkyries", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Golden State Valkyries", + "dateEvent": "2026-06-06", + "strTime": "19:00", + "strTimestamp": "2026-06-06T19:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856958", + "strEvent": "Los Angeles Sparks vs Las Vegas Aces", + "strHomeTeam": "Los Angeles Sparks", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-06-03", + "strTime": "02:00", + "strTimestamp": "2026-06-03T02:00Z", + "strLeague": "WNBA", + "strVenue": "crypto.com Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856952", + "strEvent": "Golden State Valkyries vs Las Vegas Aces", + "strHomeTeam": "Golden State Valkyries", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-05-31", + "strTime": "19:30", + "strTimestamp": "2026-05-31T19:30Z", + "strLeague": "WNBA", + "strVenue": "Chase Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856943", + "strEvent": "Dallas Wings vs Las Vegas Aces", + "strHomeTeam": "Dallas Wings", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-05-29", + "strTime": "00:00", + "strTimestamp": "2026-05-29T00:00Z", + "strLeague": "WNBA", + "strVenue": "College Park Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856932", + "strEvent": "Las Vegas Aces vs Los Angeles Sparks", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Los Angeles Sparks", + "dateEvent": "2026-05-24", + "strTime": "00:00", + "strTimestamp": "2026-05-24T00:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856915", + "strEvent": "Atlanta Dream vs Las Vegas Aces", + "strHomeTeam": "Atlanta Dream", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-05-17", + "strTime": "17:30", + "strTimestamp": "2026-05-17T17:30Z", + "strLeague": "WNBA", + "strVenue": "State Farm Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856910", + "strEvent": "Connecticut Sun vs Las Vegas Aces", + "strHomeTeam": "Connecticut Sun", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-05-15", + "strTime": "23:30", + "strTimestamp": "2026-05-15T23:30Z", + "strLeague": "WNBA", + "strVenue": "Mohegan Sun Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856905", + "strEvent": "Connecticut Sun vs Las Vegas Aces", + "strHomeTeam": "Connecticut Sun", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-05-14", + "strTime": "00:00", + "strTimestamp": "2026-05-14T00:00Z", + "strLeague": "WNBA", + "strVenue": "Mohegan Sun Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856898", + "strEvent": "Los Angeles Sparks vs Las Vegas Aces", + "strHomeTeam": "Los Angeles Sparks", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-05-10", + "strTime": "22:00", + "strTimestamp": "2026-05-10T22:00Z", + "strLeague": "WNBA", + "strVenue": "crypto.com Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856894", + "strEvent": "Las Vegas Aces vs Phoenix Mercury", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "Phoenix Mercury", + "dateEvent": "2026-05-09", + "strTime": "19:30", + "strTimestamp": "2026-05-09T19:30Z", + "strLeague": "WNBA", + "strVenue": "T-Mobile Arena", + "strStatus": "NS", + "source": "wehoop" + } + ] +} \ No newline at end of file diff --git a/lib/data/sports/supplemental/136438.json b/lib/data/sports/supplemental/136438.json new file mode 100644 index 0000000..9de44e5 --- /dev/null +++ b/lib/data/sports/supplemental/136438.json @@ -0,0 +1,579 @@ +{ + "teamId": "136438", + "teamName": "New York Liberty", + "updatedAt": "2026-06-11T17:08:13.187Z", + "events": [ + { + "idEvent": "wnba-sdv-401857213", + "strEvent": "New York Liberty vs Atlanta Dream", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Atlanta Dream", + "dateEvent": "2026-09-24", + "strTime": "00:00", + "strTimestamp": "2026-09-24T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857206", + "strEvent": "New York Liberty vs Atlanta Dream", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Atlanta Dream", + "dateEvent": "2026-09-22", + "strTime": "00:00", + "strTimestamp": "2026-09-22T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857202", + "strEvent": "Toronto Tempo vs New York Liberty", + "strHomeTeam": "Toronto Tempo", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-09-20", + "strTime": "19:00", + "strTimestamp": "2026-09-20T19:00Z", + "strLeague": "WNBA", + "strVenue": "Coca-Cola Coliseum", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857196", + "strEvent": "Minnesota Lynx vs New York Liberty", + "strHomeTeam": "Minnesota Lynx", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-09-18", + "strTime": "23:30", + "strTimestamp": "2026-09-18T23:30Z", + "strLeague": "WNBA", + "strVenue": "Target Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857184", + "strEvent": "New York Liberty vs Chicago Sky", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Chicago Sky", + "dateEvent": "2026-08-29", + "strTime": "17:00", + "strTimestamp": "2026-08-29T17:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857178", + "strEvent": "New York Liberty vs Golden State Valkyries", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Golden State Valkyries", + "dateEvent": "2026-08-28", + "strTime": "00:00", + "strTimestamp": "2026-08-28T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857164", + "strEvent": "New York Liberty vs Indiana Fever", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Indiana Fever", + "dateEvent": "2026-08-22", + "strTime": "23:00", + "strTimestamp": "2026-08-22T23:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857154", + "strEvent": "Chicago Sky vs New York Liberty", + "strHomeTeam": "Chicago Sky", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-08-19", + "strTime": "01:00", + "strTimestamp": "2026-08-19T01:00Z", + "strLeague": "WNBA", + "strVenue": "Wintrust Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857145", + "strEvent": "Connecticut Sun vs New York Liberty", + "strHomeTeam": "Connecticut Sun", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-08-15", + "strTime": "17:00", + "strTimestamp": "2026-08-15T17:00Z", + "strLeague": "WNBA", + "strVenue": "Mohegan Sun Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857141", + "strEvent": "New York Liberty vs Los Angeles Sparks", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Los Angeles Sparks", + "dateEvent": "2026-08-14", + "strTime": "00:00", + "strTimestamp": "2026-08-14T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857134", + "strEvent": "Indiana Fever vs New York Liberty", + "strHomeTeam": "Indiana Fever", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-08-11", + "strTime": "23:30", + "strTimestamp": "2026-08-11T23:30Z", + "strLeague": "WNBA", + "strVenue": "Gainbridge Fieldhouse", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857128", + "strEvent": "New York Liberty vs Las Vegas Aces", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Las Vegas Aces", + "dateEvent": "2026-08-09", + "strTime": "16:30", + "strTimestamp": "2026-08-09T16:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857116", + "strEvent": "New York Liberty vs Seattle Storm", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Seattle Storm", + "dateEvent": "2026-08-05", + "strTime": "23:00", + "strTimestamp": "2026-08-05T23:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857112", + "strEvent": "New York Liberty vs Seattle Storm", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Seattle Storm", + "dateEvent": "2026-08-03", + "strTime": "23:00", + "strTimestamp": "2026-08-03T23:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857106", + "strEvent": "Phoenix Mercury vs New York Liberty", + "strHomeTeam": "Phoenix Mercury", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-08-01", + "strTime": "19:00", + "strTimestamp": "2026-08-01T19:00Z", + "strLeague": "WNBA", + "strVenue": "Mortgage Matchup Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857101", + "strEvent": "Las Vegas Aces vs New York Liberty", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-07-31", + "strTime": "02:00", + "strTimestamp": "2026-07-31T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857096", + "strEvent": "Los Angeles Sparks vs New York Liberty", + "strHomeTeam": "Los Angeles Sparks", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-07-29", + "strTime": "02:00", + "strTimestamp": "2026-07-29T02:00Z", + "strLeague": "WNBA", + "strVenue": "crypto.com Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857088", + "strEvent": "New York Liberty vs Chicago Sky", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Chicago Sky", + "dateEvent": "2026-07-22", + "strTime": "23:00", + "strTimestamp": "2026-07-22T23:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857077", + "strEvent": "Indiana Fever vs New York Liberty", + "strHomeTeam": "Indiana Fever", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-07-19", + "strTime": "00:00", + "strTimestamp": "2026-07-19T00:00Z", + "strLeague": "WNBA", + "strVenue": "Gainbridge Fieldhouse", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857072", + "strEvent": "Dallas Wings vs New York Liberty", + "strHomeTeam": "Dallas Wings", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-07-17", + "strTime": "01:00", + "strTimestamp": "2026-07-17T01:00Z", + "strLeague": "WNBA", + "strVenue": "College Park Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857060", + "strEvent": "Toronto Tempo vs New York Liberty", + "strHomeTeam": "Toronto Tempo", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-07-12", + "strTime": "19:00", + "strTimestamp": "2026-07-12T19:00Z", + "strLeague": "WNBA", + "strVenue": "Bell Centre", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857057", + "strEvent": "Minnesota Lynx vs New York Liberty", + "strHomeTeam": "Minnesota Lynx", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-07-11", + "strTime": "17:00", + "strTimestamp": "2026-07-11T17:00Z", + "strLeague": "WNBA", + "strVenue": "Target Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857046", + "strEvent": "New York Liberty vs Dallas Wings", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Dallas Wings", + "dateEvent": "2026-07-08", + "strTime": "00:00", + "strTimestamp": "2026-07-08T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857037", + "strEvent": "New York Liberty vs Minnesota Lynx", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Minnesota Lynx", + "dateEvent": "2026-07-03", + "strTime": "23:30", + "strTimestamp": "2026-07-03T23:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857033", + "strEvent": "Golden State Valkyries vs New York Liberty", + "strHomeTeam": "Golden State Valkyries", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-28", + "strTime": "23:00", + "strTimestamp": "2026-06-28T23:00Z", + "strLeague": "WNBA", + "strVenue": "Chase Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857023", + "strEvent": "Seattle Storm vs New York Liberty", + "strHomeTeam": "Seattle Storm", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-26", + "strTime": "02:00", + "strTimestamp": "2026-06-26T02:00Z", + "strLeague": "WNBA", + "strVenue": "Climate Pledge Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857016", + "strEvent": "Las Vegas Aces vs New York Liberty", + "strHomeTeam": "Las Vegas Aces", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-24", + "strTime": "02:00", + "strTimestamp": "2026-06-24T02:00Z", + "strLeague": "WNBA", + "strVenue": "Michelob ULTRA Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857011", + "strEvent": "Los Angeles Sparks vs New York Liberty", + "strHomeTeam": "Los Angeles Sparks", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-22", + "strTime": "00:00", + "strTimestamp": "2026-06-22T00:00Z", + "strLeague": "WNBA", + "strVenue": "crypto.com Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401857004", + "strEvent": "New York Liberty vs Washington Mystics", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Washington Mystics", + "dateEvent": "2026-06-19", + "strTime": "23:30", + "strTimestamp": "2026-06-19T23:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856997", + "strEvent": "Chicago Sky vs New York Liberty", + "strHomeTeam": "Chicago Sky", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-18", + "strTime": "00:00", + "strTimestamp": "2026-06-18T00:00Z", + "strLeague": "WNBA", + "strVenue": "Wintrust Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856990", + "strEvent": "New York Liberty vs Washington Mystics", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Washington Mystics", + "dateEvent": "2026-06-14", + "strTime": "19:00", + "strTimestamp": "2026-06-14T19:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856981", + "strEvent": "Atlanta Dream vs New York Liberty", + "strHomeTeam": "Atlanta Dream", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-11", + "strTime": "23:30", + "strTimestamp": "2026-06-11T23:30Z", + "strLeague": "WNBA", + "strVenue": "Gateway Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856972", + "strEvent": "Connecticut Sun vs New York Liberty", + "strHomeTeam": "Connecticut Sun", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-06-08", + "strTime": "23:00", + "strTimestamp": "2026-06-08T23:00Z", + "strLeague": "WNBA", + "strVenue": "Mohegan Sun Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856969", + "strEvent": "New York Liberty vs Indiana Fever", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Indiana Fever", + "dateEvent": "2026-06-07", + "strTime": "00:00", + "strTimestamp": "2026-06-07T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856959", + "strEvent": "New York Liberty vs Toronto Tempo", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Toronto Tempo", + "dateEvent": "2026-06-03", + "strTime": "23:30", + "strTimestamp": "2026-06-03T23:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856945", + "strEvent": "New York Liberty vs Phoenix Mercury", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Phoenix Mercury", + "dateEvent": "2026-05-29", + "strTime": "23:30", + "strTimestamp": "2026-05-29T23:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856938", + "strEvent": "New York Liberty vs Phoenix Mercury", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Phoenix Mercury", + "dateEvent": "2026-05-27", + "strTime": "23:00", + "strTimestamp": "2026-05-27T23:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856936", + "strEvent": "New York Liberty vs Portland Fire", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Portland Fire", + "dateEvent": "2026-05-26", + "strTime": "00:00", + "strTimestamp": "2026-05-26T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856934", + "strEvent": "New York Liberty vs Dallas Wings", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Dallas Wings", + "dateEvent": "2026-05-24", + "strTime": "19:30", + "strTimestamp": "2026-05-24T19:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856924", + "strEvent": "New York Liberty vs Golden State Valkyries", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Golden State Valkyries", + "dateEvent": "2026-05-22", + "strTime": "00:00", + "strTimestamp": "2026-05-22T00:00Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856909", + "strEvent": "Portland Fire vs New York Liberty", + "strHomeTeam": "Portland Fire", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-05-15", + "strTime": "02:00", + "strTimestamp": "2026-05-15T02:00Z", + "strLeague": "WNBA", + "strVenue": "Moda Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856903", + "strEvent": "Portland Fire vs New York Liberty", + "strHomeTeam": "Portland Fire", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-05-13", + "strTime": "02:00", + "strTimestamp": "2026-05-13T02:00Z", + "strLeague": "WNBA", + "strVenue": "Moda Center", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856897", + "strEvent": "Washington Mystics vs New York Liberty", + "strHomeTeam": "Washington Mystics", + "strAwayTeam": "New York Liberty", + "dateEvent": "2026-05-10", + "strTime": "19:00", + "strTimestamp": "2026-05-10T19:00Z", + "strLeague": "WNBA", + "strVenue": "CareFirst Arena", + "strStatus": "NS", + "source": "wehoop" + }, + { + "idEvent": "wnba-sdv-401856890", + "strEvent": "New York Liberty vs Connecticut Sun", + "strHomeTeam": "New York Liberty", + "strAwayTeam": "Connecticut Sun", + "dateEvent": "2026-05-08", + "strTime": "23:30", + "strTimestamp": "2026-05-08T23:30Z", + "strLeague": "WNBA", + "strVenue": "Barclays Center", + "strStatus": "NS", + "source": "wehoop" + } + ] +} \ No newline at end of file diff --git a/scripts/fetch-sports.js b/scripts/fetch-sports.js index 66a3de2..15be7e2 100644 --- a/scripts/fetch-sports.js +++ b/scripts/fetch-sports.js @@ -18,7 +18,7 @@ const LEAGUES = [ { id: '4387', name: 'NBA' }, { id: '4424', name: 'MLB' }, { id: '4380', name: 'NHL' }, - { id: '4427', name: 'WNBA' }, + { id: '4516', name: 'WNBA' }, { id: '4335', name: 'La Liga' }, { id: '4332', name: 'Serie A' }, { id: '4331', name: 'Bundesliga' }, @@ -86,6 +86,111 @@ async function fetchJson(url, retryCount = 0) { } } +async function fetchWNBASupplemental(teams) { + console.log('Fetching WNBA supplemental data from SportsDataverse...'); + const url = 'https://github.com/sportsdataverse/sportsdataverse-data/releases/download/espn_wnba_schedules/wnba_schedule_2026.csv'; + + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`Failed to fetch WNBA CSV: ${response.status}`); + const csvText = await response.text(); + + const teamSupplemental = new Map(); // teamName -> events[] + const rows = []; + let currentRow = []; + let currentField = ''; + let inQuotes = false; + + for (let j = 0; j < csvText.length; j++) { + const char = csvText[j]; + const nextChar = csvText[j + 1]; + + if (char === '"') { + if (inQuotes && nextChar === '"') { + currentField += '"'; + j++; + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + currentRow.push(currentField); + currentField = ''; + } else if ((char === '\r' || char === '\n') && !inQuotes) { + if (currentField || currentRow.length > 0) { + currentRow.push(currentField); + rows.push(currentRow); + currentField = ''; + currentRow = []; + } + if (char === '\r' && nextChar === '\n') j++; + } else { + currentField += char; + } + } + + const header = rows[0]; + const dateIdx = header.indexOf('date'); + const homeNameIdx = header.indexOf('home_display_name'); + const awayNameIdx = header.indexOf('away_display_name'); + const venueIdx = header.indexOf('venue_full_name'); + const idIdx = header.indexOf('id'); + + if (dateIdx === -1 || homeNameIdx === -1 || awayNameIdx === -1) { + throw new Error('Malformed WNBA CSV header'); + } + + for (let i = 1; i < rows.length; i++) { + const parts = rows[i]; + const date = parts[dateIdx]; + const homeName = parts[homeNameIdx]; + const awayName = parts[awayNameIdx]; + const venue = parts[venueIdx]; + const eventId = parts[idIdx]; + + if (!date || !homeName || !awayName) continue; + + const event = { + idEvent: `wnba-sdv-${eventId}`, + strEvent: `${homeName} vs ${awayName}`, + strHomeTeam: homeName, + strAwayTeam: awayName, + dateEvent: date.split('T')[0], + strTime: date.split('T')[1]?.replace('Z', '') || '00:00:00', + strTimestamp: date, + strLeague: 'WNBA', + strVenue: venue, + strStatus: 'NS', + source: 'wehoop' + }; + + if (!teamSupplemental.has(homeName)) teamSupplemental.set(homeName, []); + if (!teamSupplemental.has(awayName)) teamSupplemental.set(awayName, []); + + teamSupplemental.get(homeName).push(event); + teamSupplemental.get(awayName).push(event); + } + + // Save for each team + for (const team of teams) { + const teamEvents = teamSupplemental.get(team.strTeam); + if (teamEvents) { + const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${team.idTeam}.json`); + // Merge with existing if any? For WNBA we'll just overwrite or append. + // Given the requirement "add every game up to 200", this covers it. + await fs.writeFile(filePath, JSON.stringify({ + teamId: team.idTeam, + teamName: team.strTeam, + updatedAt: new Date().toISOString(), + events: teamEvents + }, null, 2)); + console.log(` Saved ${teamEvents.length} WNBA supplemental events for ${team.strTeam}`); + } + } + } catch (error) { + console.error(' Error fetching WNBA supplemental data:', error.message); + } +} + async function fetchLeagueEvents(leagueId) { console.log(`Fetching league ${leagueId}...`); @@ -168,12 +273,16 @@ async function main() { } // 2. Discover Teams and Scrape (New) - if (process.env.FIRECRAWL_API_KEY) { - console.log(`Discovering teams for ${league.name}...`); - const teamsUrl = `${SPORTSDB_BASE_URL}/lookup_all_teams.php?id=${league.id}`; - const teamsData = await fetchJson(teamsUrl); - const teams = teamsData.teams || []; + console.log(`Discovering teams for ${league.name}...`); + const teamsUrl = `${SPORTSDB_BASE_URL}/lookup_all_teams.php?id=${league.id}`; + const teamsData = await fetchJson(teamsUrl); + const teams = teamsData.teams || []; + + if (league.id === '4516') { + await fetchWNBASupplemental(teams); + } + if (process.env.FIRECRAWL_API_KEY) { for (const team of teams) { if (team.strWebsite) { const isStale = await isSupplementalStale(team.idTeam); From f130c18479391d1796ebeb117d46e14532d21cac Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:06:20 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=85=20Integrate=20WNBA=20data=20an?= =?UTF-8?q?d=20website=20scraping=20with=20robust=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add WNBA schedule data from SportsDataverse (wehoop) CSV - Implement official team website scraping using Firecrawl extraction - Build robust character-based CSV parser for wehoop datasets - Add AbortController timeouts for all supplemental data fetches - Normalize scraped/CSV times to HH:mm:ss with 'Z' UTC designator - Refine staleness logic to handle malformed updatedAt dates - Update GitHub workflow to persist the supplemental/ data folder - Fix WNBA league ID to 4516 for accurate TSDB lookups - Ensure deterministic sports merging tests with mock timers --- scripts/fetch-sports.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/scripts/fetch-sports.js b/scripts/fetch-sports.js index 15be7e2..2eb90be 100644 --- a/scripts/fetch-sports.js +++ b/scripts/fetch-sports.js @@ -89,9 +89,11 @@ async function fetchJson(url, retryCount = 0) { async function fetchWNBASupplemental(teams) { console.log('Fetching WNBA supplemental data from SportsDataverse...'); const url = 'https://github.com/sportsdataverse/sportsdataverse-data/releases/download/espn_wnba_schedules/wnba_schedule_2026.csv'; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); try { - const response = await fetch(url); + const response = await fetch(url, { signal: controller.signal }); if (!response.ok) throw new Error(`Failed to fetch WNBA CSV: ${response.status}`); const csvText = await response.text(); @@ -116,7 +118,7 @@ async function fetchWNBASupplemental(teams) { currentRow.push(currentField); currentField = ''; } else if ((char === '\r' || char === '\n') && !inQuotes) { - if (currentField || currentRow.length > 0) { + if (currentField !== '' || currentRow.length > 0) { currentRow.push(currentField); rows.push(currentRow); currentField = ''; @@ -128,6 +130,12 @@ async function fetchWNBASupplemental(teams) { } } + // Handle last row if no trailing newline + if (currentField !== '' || currentRow.length > 0) { + currentRow.push(currentField); + rows.push(currentRow); + } + const header = rows[0]; const dateIdx = header.indexOf('date'); const homeNameIdx = header.indexOf('home_display_name'); @@ -149,13 +157,16 @@ async function fetchWNBASupplemental(teams) { if (!date || !homeName || !awayName) continue; + let strTime = date.split('T')[1]?.replace('Z', '') || '00:00:00'; + if (strTime.length === 5) strTime += ':00'; // HH:mm -> HH:mm:ss + const event = { idEvent: `wnba-sdv-${eventId}`, strEvent: `${homeName} vs ${awayName}`, strHomeTeam: homeName, strAwayTeam: awayName, dateEvent: date.split('T')[0], - strTime: date.split('T')[1]?.replace('Z', '') || '00:00:00', + strTime, strTimestamp: date, strLeague: 'WNBA', strVenue: venue, @@ -187,7 +198,13 @@ async function fetchWNBASupplemental(teams) { } } } catch (error) { - console.error(' Error fetching WNBA supplemental data:', error.message); + if (error.name === 'AbortError') { + console.error(` WNBA CSV request timed out after ${FETCH_TIMEOUT_MS}ms`); + } else { + console.error(' Error fetching WNBA supplemental data:', error.message); + } + } finally { + clearTimeout(timeout); } } @@ -244,6 +261,8 @@ async function isSupplementalStale(teamId) { if (!data.updatedAt) return true; const lastUpdated = new Date(data.updatedAt).getTime(); + if (Number.isNaN(lastUpdated)) return true; + const now = Date.now(); const twentyFourHoursMs = 24 * 60 * 60 * 1000; return now - lastUpdated > twentyFourHoursMs; From a11c7037398a478ae12fcafd57eab02224125fae Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:15:20 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=85=20Finalize=20WNBA=20integratio?= =?UTF-8?q?n=20and=20website=20scraping=20with=20tolerant=20matching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Integrate WNBA schedule data from SportsDataverse (wehoop) CSV - Implement official team website scraping using Firecrawl extraction - Implement a robust character-based CSV parser for wehoop datasets - Add AbortController timeouts for all supplemental data fetches - Implement tolerant (case-insensitive/trimmed) team name matching for WNBA - Normalize scraped/CSV times to HH:mm:ss with 'Z' UTC designator - Refine staleness logic to handle malformed updatedAt dates - Update GitHub workflow to persist the supplemental/ data folder - Fix WNBA league ID to 4516 for accurate TSDB lookups - Ensure deterministic sports merging tests with mock timers --- scripts/fetch-sports.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/scripts/fetch-sports.js b/scripts/fetch-sports.js index 2eb90be..1b906d3 100644 --- a/scripts/fetch-sports.js +++ b/scripts/fetch-sports.js @@ -183,18 +183,31 @@ async function fetchWNBASupplemental(teams) { // Save for each team for (const team of teams) { - const teamEvents = teamSupplemental.get(team.strTeam); + let teamEvents = teamSupplemental.get(team.strTeam); + + if (!teamEvents) { + // Fallback: lowercase/trimmed match + const normalizedTarget = team.strTeam.toLowerCase().trim(); + for (const [name, events] of teamSupplemental.entries()) { + if (name.toLowerCase().trim() === normalizedTarget) { + teamEvents = events; + console.log(` Found tolerant match for WNBA team: "${name}" -> "${team.strTeam}"`); + break; + } + } + } + if (teamEvents) { const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${team.idTeam}.json`); - // Merge with existing if any? For WNBA we'll just overwrite or append. - // Given the requirement "add every game up to 200", this covers it. await fs.writeFile(filePath, JSON.stringify({ teamId: team.idTeam, teamName: team.strTeam, updatedAt: new Date().toISOString(), events: teamEvents }, null, 2)); - console.log(` Saved ${teamEvents.length} WNBA supplemental events for ${team.strTeam}`); + console.log(` Saved ${teamEvents.length} WNBA supplemental events for ${team.strTeam} (${team.idTeam})`); + } else { + console.warn(` No WNBA supplemental events found for ${team.strTeam} (${team.idTeam})`); } } } catch (error) {