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/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/lib/sports.js b/lib/sports.js index b87c15e..066964b 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,91 @@ 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, '-'); + + // 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${normalizedTime}Z`; + } + + 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: normalizedTime, + 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 controller = new AbortController(); + const timeoutMs = parseInt(env.FIRECRAWL_TIMEOUT_MS, 10) || 10000; + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + 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' + }, + 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; + } 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) { try { const filePath = path.join(DATA_DIR, `${leagueId}.json`); @@ -75,6 +185,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 +250,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..1b906d3 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; @@ -16,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' }, @@ -84,6 +86,141 @@ 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, { signal: controller.signal }); + 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; + } + } + + // 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'); + 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; + + 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, + 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) { + 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`); + 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} (${team.idTeam})`); + } else { + console.warn(` No WNBA supplemental events found for ${team.strTeam} (${team.idTeam})`); + } + } + } catch (error) { + 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); + } +} + async function fetchLeagueEvents(leagueId) { console.log(`Fetching league ${leagueId}...`); @@ -129,11 +266,32 @@ async function fetchLeagueEvents(leagueId) { return allEvents; } +async function isSupplementalStale(teamId) { + try { + const filePath = path.join(SUPPLEMENTAL_DATA_DIR, `${teamId}.json`); + const content = await fs.readFile(filePath, 'utf8'); + const data = JSON.parse(content); + 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; + } catch (error) { + return true; // File doesn't exist or is invalid + } +} + + 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 +303,46 @@ async function main() { }, null, 2)); console.log(`Saved ${events.length} events for ${league.name} to ${filePath}`); } + + // 2. Discover Teams and Scrape (New) + 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); + if (isStale) { + console.log(` Scraping ${team.strTeam} website: ${team.strWebsite}...`); + try { + const games = await fetchScheduleFromWebsite(team.strWebsite); + 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) { + 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..d1e1215 100644 --- a/test/sports.test.js +++ b/test/sports.test.js @@ -197,6 +197,52 @@ 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`); + 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') });