Remove CFB/NASCAR/PGA and streamline to 8 supported sports

- Remove College Football, NASCAR, and PGA from scraper and app
- Clean all data files (stadiums, games, pipeline reports)
- Update Sport.swift enum and all UI components
- Add sportstime.py CLI tool for pipeline management
- Add DATA_SCRAPING.md documentation
- Add WNBA/MLS/NWSL implementation documentation
- Scraper now supports: NBA, MLB, NHL, NFL, WNBA, MLS, NWSL, CBB

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-09 23:22:13 -06:00
parent f5e509a9ae
commit 8790d2ad73
35 changed files with 117819 additions and 65871 deletions

View File

@@ -7,24 +7,43 @@ Imports canonical JSON data into CloudKit. Run after canonicalization pipeline.
Expected input files (from canonicalization pipeline):
- stadiums_canonical.json
- teams_canonical.json
- games_canonical.json
- games_canonical.json OR canonical/games/*.json (new structure)
- stadium_aliases.json
- league_structure.json
- team_aliases.json
File Structure (Option B - by sport/season):
data/
games/ # Raw scraped games
mlb_2025.json
nba_2025.json
...
canonical/ # Canonicalized data
games/
mlb_2025.json
nba_2025.json
...
stadiums.json
games_canonical.json # Combined (backward compatibility)
stadiums_canonical.json
teams_canonical.json
Setup:
1. CloudKit Dashboard > Tokens & Keys > Server-to-Server Keys
2. Create key with Read/Write access to public database
3. Download .p8 file and note Key ID
Usage:
python cloudkit_import.py --dry-run # Preview first
python cloudkit_import.py --key-id XX --key-file key.p8 # Import all
python cloudkit_import.py --stadiums-only ... # Stadiums first
python cloudkit_import.py --games-only ... # Games after
python cloudkit_import.py --stadium-aliases-only ... # Stadium aliases only
python cloudkit_import.py --delete-all ... # Delete then import
python cloudkit_import.py --delete-only ... # Delete only (no import)
python cloudkit_import.py # Interactive menu
python cloudkit_import.py --dry-run # Preview first
python cloudkit_import.py --key-id XX --key-file key.p8 # Import all
python cloudkit_import.py --stadiums-only # Stadiums first
python cloudkit_import.py --games-only # All games
python cloudkit_import.py --games-files mlb_2025.json # Specific game file
python cloudkit_import.py --games-files mlb_2025.json,nba_2025.json # Multiple files
python cloudkit_import.py --stadium-aliases-only # Stadium aliases only
python cloudkit_import.py --delete-all # Delete then import
python cloudkit_import.py --delete-only # Delete only (no import)
"""
import argparse, json, time, os, sys, hashlib, base64, requests
@@ -48,6 +67,58 @@ DEFAULT_KEY_ID = "152be0715e0276e31aaea5cbfe79dc872f298861a55c70fae14e5fe3e026cf
DEFAULT_KEY_FILE = "eckey.pem"
def show_game_files_menu(data_dir: Path) -> list[str]:
"""Show available game files and let user select which to import."""
canonical_games_dir = data_dir / 'canonical' / 'games'
if not canonical_games_dir.exists():
print("\n No canonical/games/ directory found.")
return []
game_files = sorted(canonical_games_dir.glob('*.json'))
if not game_files:
print("\n No game files found in canonical/games/")
return []
print("\n" + "="*50)
print("Select Game Files to Import")
print("="*50)
print("\n Available files:")
for i, f in enumerate(game_files, 1):
# Count games in file
with open(f) as fp:
games = json.load(fp)
print(f" {i}. {f.name} ({len(games):,} games)")
print(f"\n a. All files")
print(f" 0. Cancel")
print()
while True:
try:
choice = input("Enter file numbers (comma-separated), 'a' for all, or 0 to cancel: ").strip().lower()
if choice == '0':
return []
if choice == 'a':
return [f.name for f in game_files]
# Parse comma-separated numbers
indices = [int(x.strip()) for x in choice.split(',')]
selected = []
for idx in indices:
if 1 <= idx <= len(game_files):
selected.append(game_files[idx-1].name)
else:
print(f"Invalid selection: {idx}")
continue
if selected:
return selected
print("No valid selections. Try again.")
except (ValueError, EOFError, KeyboardInterrupt):
print("\nCancelled.")
return []
def show_menu():
"""Show interactive menu and return selected action."""
print("\n" + "="*50)
@@ -55,25 +126,26 @@ def show_menu():
print("="*50)
print("\n 1. Import all (stadiums, teams, games, league structure, team aliases, stadium aliases)")
print(" 2. Stadiums only")
print(" 3. Games only")
print(" 4. League structure only")
print(" 5. Team aliases only")
print(" 6. Stadium aliases only")
print(" 7. Canonical only (league structure + team aliases + stadium aliases)")
print(" 8. Delete all then import")
print(" 9. Delete only (no import)")
print(" 10. Dry run (preview only)")
print(" 3. Games only (all files)")
print(" 4. Games - select specific files")
print(" 5. League structure only")
print(" 6. Team aliases only")
print(" 7. Stadium aliases only")
print(" 8. Canonical only (league structure + team aliases + stadium aliases)")
print(" 9. Delete all then import")
print(" 10. Delete only (no import)")
print(" 11. Dry run (preview only)")
print(" 0. Exit")
print()
while True:
try:
choice = input("Enter choice [1-10, 0 to exit]: ").strip()
choice = input("Enter choice [1-11, 0 to exit]: ").strip()
if choice == '0':
return None
if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']:
if choice in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']:
return int(choice)
print("Invalid choice. Please enter 1-10 or 0.")
print("Invalid choice. Please enter 1-11 or 0.")
except (EOFError, KeyboardInterrupt):
print("\nExiting.")
return None
@@ -265,6 +337,7 @@ def main():
p.add_argument('--data-dir', default='./data')
p.add_argument('--stadiums-only', action='store_true')
p.add_argument('--games-only', action='store_true')
p.add_argument('--games-files', type=str, help='Comma-separated list of game files to import (e.g., mlb_2025.json,nba_2025.json)')
p.add_argument('--league-structure-only', action='store_true', help='Import only league structure')
p.add_argument('--team-aliases-only', action='store_true', help='Import only team aliases')
p.add_argument('--stadium-aliases-only', action='store_true', help='Import only stadium aliases')
@@ -278,11 +351,18 @@ def main():
# Show interactive menu if no action flags provided or --interactive
has_action_flag = any([
args.stadiums_only, args.games_only, args.league_structure_only,
args.stadiums_only, args.games_only, args.games_files, args.league_structure_only,
args.team_aliases_only, args.stadium_aliases_only, args.canonical_only,
args.delete_all, args.delete_only, args.dry_run
])
# Track selected game files (for option 4 or --games-files)
selected_game_files = None
if args.games_files:
# Parse comma-separated list from command line
selected_game_files = [f.strip() for f in args.games_files.split(',')]
args.games_only = True # Imply --games-only
if args.interactive or not has_action_flag:
choice = show_menu()
if choice is None:
@@ -293,21 +373,27 @@ def main():
pass # Default behavior
elif choice == 2: # Stadiums only
args.stadiums_only = True
elif choice == 3: # Games only
elif choice == 3: # Games only (all files)
args.games_only = True
elif choice == 4: # League structure only
elif choice == 4: # Games - select specific files
args.games_only = True
selected_game_files = show_game_files_menu(Path(args.data_dir))
if not selected_game_files:
print("No files selected. Exiting.")
return
elif choice == 5: # League structure only
args.league_structure_only = True
elif choice == 5: # Team aliases only
elif choice == 6: # Team aliases only
args.team_aliases_only = True
elif choice == 6: # Stadium aliases only
elif choice == 7: # Stadium aliases only
args.stadium_aliases_only = True
elif choice == 7: # Canonical only
elif choice == 8: # Canonical only
args.canonical_only = True
elif choice == 8: # Delete all then import
elif choice == 9: # Delete all then import
args.delete_all = True
elif choice == 9: # Delete only
elif choice == 10: # Delete only
args.delete_only = True
elif choice == 10: # Dry run
elif choice == 11: # Dry run
args.dry_run = True
print(f"\n{'='*50}")
@@ -332,12 +418,34 @@ def main():
else:
teams = [] # Legacy: extracted from stadiums
if (data_dir / 'games_canonical.json').exists():
# Load games: try new structure first (canonical/games/*.json), then fallback
canonical_games_dir = data_dir / 'canonical' / 'games'
games = []
games_source = None
if selected_game_files:
# Load only the selected files
for filename in selected_game_files:
filepath = canonical_games_dir / filename
if filepath.exists():
with open(filepath) as f:
file_games = json.load(f)
games.extend(file_games)
print(f" Loading {filename}: {len(file_games):,} games")
games_source = f"selected files: {', '.join(selected_game_files)}"
elif canonical_games_dir.exists() and any(canonical_games_dir.glob('*.json')):
# New structure: load all sport/season files
for games_file in sorted(canonical_games_dir.glob('*.json')):
with open(games_file) as f:
file_games = json.load(f)
games.extend(file_games)
games_source = "canonical/games/*.json"
elif (data_dir / 'games_canonical.json').exists():
games = json.load(open(data_dir / 'games_canonical.json'))
games_source = "games_canonical.json"
elif (data_dir / 'games.json').exists():
games = json.load(open(data_dir / 'games.json'))
else:
games = []
games_source = "games.json (legacy)"
league_structure = json.load(open(data_dir / 'league_structure.json')) if (data_dir / 'league_structure.json').exists() else []
team_aliases = json.load(open(data_dir / 'team_aliases.json')) if (data_dir / 'team_aliases.json').exists() else []
@@ -345,6 +453,8 @@ def main():
print(f"Using {'canonical' if use_canonical else 'legacy'} format")
print(f"Loaded {len(stadiums)} stadiums, {len(teams)} teams, {len(games)} games")
if games_source:
print(f" Games loaded from: {games_source}")
print(f"Loaded {len(league_structure)} league structures, {len(team_aliases)} team aliases, {len(stadium_aliases)} stadium aliases\n")
ck = None

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,10 @@
"error_count": 0,
"warning_count": 0,
"summary": {
"stadiums": 92,
"stadiums": 148,
"teams": 92,
"games": 4972,
"aliases": 130,
"games": 0,
"aliases": 194,
"by_category": {}
},
"errors": []

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,205 @@
{
"generated_at": "2026-01-09T19:16:47.175229",
"season": 2026,
"sport": "all",
"summary": {
"games_scraped": 5768,
"stadiums_scraped": 180,
"games_by_sport": {
"NBA": 1230,
"MLB": 2430,
"NHL": 1312,
"NFL": 286,
"WNBA": 0,
"MLS": 510,
"NWSL": 0,
"CBB": 0
},
"high_severity": 0,
"medium_severity": 0,
"low_severity": 30
},
"game_validations": [],
"stadium_issues": [
{
"stadium": "State Farm Arena",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "TD Garden",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Barclays Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Spectrum Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "United Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Rocket Mortgage FieldHouse",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "American Airlines Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Ball Arena",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Little Caesars Arena",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Chase Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Toyota Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Gainbridge Fieldhouse",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Intuit Dome",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Crypto.com Arena",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "FedExForum",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Kaseya Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Fiserv Forum",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Target Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Smoothie King Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Madison Square Garden",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Paycom Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Kia Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Wells Fargo Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Footprint Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Moda Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Golden 1 Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Frost Bank Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Scotiabank Arena",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Delta Center",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
},
{
"stadium": "Capital One Arena",
"sport": "NBA",
"issue": "Missing capacity",
"severity": "low"
}
]
}

View File

@@ -575,6 +575,390 @@
"valid_from": null,
"valid_until": null
},
{
"alias_name": "gateway center arena",
"stadium_canonical_id": "stadium_wnba_gateway_center_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "wintrust arena",
"stadium_canonical_id": "stadium_wnba_wintrust_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "mohegan sun arena",
"stadium_canonical_id": "stadium_wnba_mohegan_sun_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "college park center",
"stadium_canonical_id": "stadium_wnba_college_park_center",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "chase center",
"stadium_canonical_id": "stadium_wnba_chase_center",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "gainbridge fieldhouse",
"stadium_canonical_id": "stadium_wnba_gainbridge_fieldhouse",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "michelob ultra arena",
"stadium_canonical_id": "stadium_wnba_michelob_ultra_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "crypto.com arena",
"stadium_canonical_id": "stadium_wnba_cryptocom_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "cryptocom arena",
"stadium_canonical_id": "stadium_wnba_cryptocom_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "target center",
"stadium_canonical_id": "stadium_wnba_target_center",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "barclays center",
"stadium_canonical_id": "stadium_wnba_barclays_center",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "footprint center",
"stadium_canonical_id": "stadium_wnba_footprint_center",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "climate pledge arena",
"stadium_canonical_id": "stadium_wnba_climate_pledge_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "entertainment & sports arena",
"stadium_canonical_id": "stadium_wnba_entertainment_sports_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "entertainment sports arena",
"stadium_canonical_id": "stadium_wnba_entertainment_sports_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "mercedes-benz stadium",
"stadium_canonical_id": "stadium_mls_mercedesbenz_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "mercedesbenz stadium",
"stadium_canonical_id": "stadium_mls_mercedesbenz_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "q2 stadium",
"stadium_canonical_id": "stadium_mls_q2_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "bank of america stadium",
"stadium_canonical_id": "stadium_mls_bank_of_america_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "soldier field",
"stadium_canonical_id": "stadium_mls_soldier_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "tql stadium",
"stadium_canonical_id": "stadium_mls_tql_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "dick's sporting goods park",
"stadium_canonical_id": "stadium_mls_dicks_sporting_goods_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "dicks sporting goods park",
"stadium_canonical_id": "stadium_mls_dicks_sporting_goods_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "lower.com field",
"stadium_canonical_id": "stadium_mls_lowercom_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "lowercom field",
"stadium_canonical_id": "stadium_mls_lowercom_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "toyota stadium",
"stadium_canonical_id": "stadium_mls_toyota_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "audi field",
"stadium_canonical_id": "stadium_mls_audi_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "shell energy stadium",
"stadium_canonical_id": "stadium_mls_shell_energy_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "dignity health sports park",
"stadium_canonical_id": "stadium_mls_dignity_health_sports_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "bmo stadium",
"stadium_canonical_id": "stadium_mls_bmo_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "chase stadium",
"stadium_canonical_id": "stadium_mls_chase_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "allianz field",
"stadium_canonical_id": "stadium_mls_allianz_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "stade saputo",
"stadium_canonical_id": "stadium_mls_stade_saputo",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "geodis park",
"stadium_canonical_id": "stadium_mls_geodis_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "gillette stadium",
"stadium_canonical_id": "stadium_mls_gillette_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "yankee stadium",
"stadium_canonical_id": "stadium_mls_yankee_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "red bull arena",
"stadium_canonical_id": "stadium_mls_red_bull_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "inter&co stadium",
"stadium_canonical_id": "stadium_mls_interco_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "interco stadium",
"stadium_canonical_id": "stadium_mls_interco_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "subaru park",
"stadium_canonical_id": "stadium_mls_subaru_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "providence park",
"stadium_canonical_id": "stadium_mls_providence_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "america first field",
"stadium_canonical_id": "stadium_mls_america_first_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "paypal park",
"stadium_canonical_id": "stadium_mls_paypal_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "lumen field",
"stadium_canonical_id": "stadium_mls_lumen_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "children's mercy park",
"stadium_canonical_id": "stadium_mls_childrens_mercy_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "childrens mercy park",
"stadium_canonical_id": "stadium_mls_childrens_mercy_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "citypark",
"stadium_canonical_id": "stadium_mls_citypark",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "bmo field",
"stadium_canonical_id": "stadium_mls_bmo_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "bc place",
"stadium_canonical_id": "stadium_mls_bc_place",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "snapdragon stadium",
"stadium_canonical_id": "stadium_mls_snapdragon_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "bmo stadium",
"stadium_canonical_id": "stadium_nwsl_bmo_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "paypal park",
"stadium_canonical_id": "stadium_nwsl_paypal_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "seatgeek stadium",
"stadium_canonical_id": "stadium_nwsl_seatgeek_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "shell energy stadium",
"stadium_canonical_id": "stadium_nwsl_shell_energy_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "cpkc stadium",
"stadium_canonical_id": "stadium_nwsl_cpkc_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "red bull arena",
"stadium_canonical_id": "stadium_nwsl_red_bull_arena",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "wakemed soccer park",
"stadium_canonical_id": "stadium_nwsl_wakemed_soccer_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "inter&co stadium",
"stadium_canonical_id": "stadium_nwsl_interco_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "interco stadium",
"stadium_canonical_id": "stadium_nwsl_interco_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "providence park",
"stadium_canonical_id": "stadium_nwsl_providence_park",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "lumen field",
"stadium_canonical_id": "stadium_nwsl_lumen_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "snapdragon stadium",
"stadium_canonical_id": "stadium_nwsl_snapdragon_stadium",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "america first field",
"stadium_canonical_id": "stadium_nwsl_america_first_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "audi field",
"stadium_canonical_id": "stadium_nwsl_audi_field",
"valid_from": null,
"valid_until": null
},
{
"alias_name": "daikin park",
"stadium_canonical_id": "stadium_mlb_minute_maid_park",

View File

@@ -1,93 +1,181 @@
id,name,city,state,latitude,longitude,capacity,sport,team_abbrevs,source,year_opened
manual_nba_atl,State Farm Arena,Atlanta,,33.7573,-84.3963,0,NBA,['ATL'],manual,
manual_nba_bos,TD Garden,Boston,,42.3662,-71.0621,0,NBA,['BOS'],manual,
manual_nba_brk,Barclays Center,Brooklyn,,40.6826,-73.9754,0,NBA,['BRK'],manual,
manual_nba_cho,Spectrum Center,Charlotte,,35.2251,-80.8392,0,NBA,['CHO'],manual,
manual_nba_chi,United Center,Chicago,,41.8807,-87.6742,0,NBA,['CHI'],manual,
manual_nba_cle,Rocket Mortgage FieldHouse,Cleveland,,41.4965,-81.6882,0,NBA,['CLE'],manual,
manual_nba_dal,American Airlines Center,Dallas,,32.7905,-96.8103,0,NBA,['DAL'],manual,
manual_nba_den,Ball Arena,Denver,,39.7487,-105.0077,0,NBA,['DEN'],manual,
manual_nba_det,Little Caesars Arena,Detroit,,42.3411,-83.0553,0,NBA,['DET'],manual,
manual_nba_gsw,Chase Center,San Francisco,,37.768,-122.3879,0,NBA,['GSW'],manual,
manual_nba_hou,Toyota Center,Houston,,29.7508,-95.3621,0,NBA,['HOU'],manual,
manual_nba_ind,Gainbridge Fieldhouse,Indianapolis,,39.764,-86.1555,0,NBA,['IND'],manual,
manual_nba_lac,Intuit Dome,Inglewood,,33.9425,-118.3419,0,NBA,['LAC'],manual,
manual_nba_lal,Crypto.com Arena,Los Angeles,,34.043,-118.2673,0,NBA,['LAL'],manual,
manual_nba_mem,FedExForum,Memphis,,35.1382,-90.0506,0,NBA,['MEM'],manual,
manual_nba_mia,Kaseya Center,Miami,,25.7814,-80.187,0,NBA,['MIA'],manual,
manual_nba_mil,Fiserv Forum,Milwaukee,,43.0451,-87.9174,0,NBA,['MIL'],manual,
manual_nba_min,Target Center,Minneapolis,,44.9795,-93.2761,0,NBA,['MIN'],manual,
manual_nba_nop,Smoothie King Center,New Orleans,,29.949,-90.0821,0,NBA,['NOP'],manual,
manual_nba_nyk,Madison Square Garden,New York,,40.7505,-73.9934,0,NBA,['NYK'],manual,
manual_nba_okc,Paycom Center,Oklahoma City,,35.4634,-97.5151,0,NBA,['OKC'],manual,
manual_nba_orl,Kia Center,Orlando,,28.5392,-81.3839,0,NBA,['ORL'],manual,
manual_nba_phi,Wells Fargo Center,Philadelphia,,39.9012,-75.172,0,NBA,['PHI'],manual,
manual_nba_pho,Footprint Center,Phoenix,,33.4457,-112.0712,0,NBA,['PHO'],manual,
manual_nba_por,Moda Center,Portland,,45.5316,-122.6668,0,NBA,['POR'],manual,
manual_nba_sac,Golden 1 Center,Sacramento,,38.5802,-121.4997,0,NBA,['SAC'],manual,
manual_nba_sas,Frost Bank Center,San Antonio,,29.427,-98.4375,0,NBA,['SAS'],manual,
manual_nba_tor,Scotiabank Arena,Toronto,,43.6435,-79.3791,0,NBA,['TOR'],manual,
manual_nba_uta,Delta Center,Salt Lake City,,40.7683,-111.9011,0,NBA,['UTA'],manual,
manual_nba_was,Capital One Arena,Washington,,38.8982,-77.0209,0,NBA,['WAS'],manual,
manual_mlb_ari,Chase Field,Phoenix,AZ,33.4453,-112.0667,48686,MLB,['ARI'],manual,
manual_mlb_atl,Truist Park,Atlanta,GA,33.8907,-84.4678,41084,MLB,['ATL'],manual,
manual_mlb_bal,Oriole Park at Camden Yards,Baltimore,MD,39.2838,-76.6218,45971,MLB,['BAL'],manual,
manual_mlb_bos,Fenway Park,Boston,MA,42.3467,-71.0972,37755,MLB,['BOS'],manual,
manual_mlb_chc,Wrigley Field,Chicago,IL,41.9484,-87.6553,41649,MLB,['CHC'],manual,
manual_mlb_chw,Guaranteed Rate Field,Chicago,IL,41.8299,-87.6338,40615,MLB,['CHW'],manual,
manual_mlb_cin,Great American Ball Park,Cincinnati,OH,39.0979,-84.5082,42319,MLB,['CIN'],manual,
manual_mlb_cle,Progressive Field,Cleveland,OH,41.4962,-81.6852,34830,MLB,['CLE'],manual,
manual_mlb_col,Coors Field,Denver,CO,39.7559,-104.9942,50144,MLB,['COL'],manual,
manual_mlb_det,Comerica Park,Detroit,MI,42.339,-83.0485,41083,MLB,['DET'],manual,
manual_mlb_hou,Minute Maid Park,Houston,TX,29.7573,-95.3555,41168,MLB,['HOU'],manual,
manual_mlb_kcr,Kauffman Stadium,Kansas City,MO,39.0517,-94.4803,37903,MLB,['KCR'],manual,
manual_mlb_laa,Angel Stadium,Anaheim,CA,33.8003,-117.8827,45517,MLB,['LAA'],manual,
manual_mlb_lad,Dodger Stadium,Los Angeles,CA,34.0739,-118.24,56000,MLB,['LAD'],manual,
manual_mlb_mia,LoanDepot Park,Miami,FL,25.7781,-80.2196,36742,MLB,['MIA'],manual,
manual_mlb_mil,American Family Field,Milwaukee,WI,43.028,-87.9712,41900,MLB,['MIL'],manual,
manual_mlb_min,Target Field,Minneapolis,MN,44.9817,-93.2776,38544,MLB,['MIN'],manual,
manual_mlb_nym,Citi Field,New York,NY,40.7571,-73.8458,41922,MLB,['NYM'],manual,
manual_mlb_nyy,Yankee Stadium,New York,NY,40.8296,-73.9262,46537,MLB,['NYY'],manual,
manual_mlb_oak,Sutter Health Park,Sacramento,CA,38.5802,-121.5097,14014,MLB,['OAK'],manual,
manual_mlb_phi,Citizens Bank Park,Philadelphia,PA,39.9061,-75.1665,42792,MLB,['PHI'],manual,
manual_mlb_pit,PNC Park,Pittsburgh,PA,40.4469,-80.0057,38362,MLB,['PIT'],manual,
manual_mlb_sdp,Petco Park,San Diego,CA,32.7076,-117.157,40209,MLB,['SDP'],manual,
manual_mlb_sfg,Oracle Park,San Francisco,CA,37.7786,-122.3893,41265,MLB,['SFG'],manual,
manual_mlb_sea,T-Mobile Park,Seattle,WA,47.5914,-122.3325,47929,MLB,['SEA'],manual,
manual_mlb_stl,Busch Stadium,St. Louis,MO,38.6226,-90.1928,45494,MLB,['STL'],manual,
manual_mlb_tbr,Tropicana Field,St. Petersburg,FL,27.7682,-82.6534,25000,MLB,['TBR'],manual,
manual_mlb_tex,Globe Life Field,Arlington,TX,32.7473,-97.0845,40300,MLB,['TEX'],manual,
manual_mlb_tor,Rogers Centre,Toronto,ON,43.6414,-79.3894,49282,MLB,['TOR'],manual,
manual_mlb_wsn,Nationals Park,Washington,DC,38.873,-77.0074,41339,MLB,['WSN'],manual,
manual_nhl_ana,Honda Center,Anaheim,CA,33.8078,-117.8765,17174,NHL,['ANA'],manual,
manual_nhl_ari,Delta Center,Salt Lake City,UT,40.7683,-111.9011,18306,NHL,['ARI'],manual,
manual_nhl_bos,TD Garden,Boston,MA,42.3662,-71.0621,17565,NHL,['BOS'],manual,
manual_nhl_buf,KeyBank Center,Buffalo,NY,42.875,-78.8764,19070,NHL,['BUF'],manual,
manual_nhl_cgy,Scotiabank Saddledome,Calgary,AB,51.0374,-114.0519,19289,NHL,['CGY'],manual,
manual_nhl_car,PNC Arena,Raleigh,NC,35.8034,-78.722,18680,NHL,['CAR'],manual,
manual_nhl_chi,United Center,Chicago,IL,41.8807,-87.6742,19717,NHL,['CHI'],manual,
manual_nhl_col,Ball Arena,Denver,CO,39.7487,-105.0077,18007,NHL,['COL'],manual,
manual_nhl_cbj,Nationwide Arena,Columbus,OH,39.9693,-83.0061,18500,NHL,['CBJ'],manual,
manual_nhl_dal,American Airlines Center,Dallas,TX,32.7905,-96.8103,18532,NHL,['DAL'],manual,
manual_nhl_det,Little Caesars Arena,Detroit,MI,42.3411,-83.0553,19515,NHL,['DET'],manual,
manual_nhl_edm,Rogers Place,Edmonton,AB,53.5469,-113.4978,18347,NHL,['EDM'],manual,
manual_nhl_fla,Amerant Bank Arena,Sunrise,FL,26.1584,-80.3256,19250,NHL,['FLA'],manual,
manual_nhl_lak,Crypto.com Arena,Los Angeles,CA,34.043,-118.2673,18230,NHL,['LAK'],manual,
manual_nhl_min,Xcel Energy Center,St. Paul,MN,44.9448,-93.101,17954,NHL,['MIN'],manual,
manual_nhl_mtl,Bell Centre,Montreal,QC,45.4961,-73.5693,21302,NHL,['MTL'],manual,
manual_nhl_nsh,Bridgestone Arena,Nashville,TN,36.1592,-86.7785,17159,NHL,['NSH'],manual,
manual_nhl_njd,Prudential Center,Newark,NJ,40.7334,-74.1712,16514,NHL,['NJD'],manual,
manual_nhl_nyi,UBS Arena,Elmont,NY,40.7161,-73.7246,17255,NHL,['NYI'],manual,
manual_nhl_nyr,Madison Square Garden,New York,NY,40.7505,-73.9934,18006,NHL,['NYR'],manual,
manual_nhl_ott,Canadian Tire Centre,Ottawa,ON,45.2969,-75.9272,18652,NHL,['OTT'],manual,
manual_nhl_phi,Wells Fargo Center,Philadelphia,PA,39.9012,-75.172,19543,NHL,['PHI'],manual,
manual_nhl_pit,PPG Paints Arena,Pittsburgh,PA,40.4395,-79.9892,18387,NHL,['PIT'],manual,
manual_nhl_sjs,SAP Center,San Jose,CA,37.3327,-121.901,17562,NHL,['SJS'],manual,
manual_nhl_sea,Climate Pledge Arena,Seattle,WA,47.6221,-122.354,17100,NHL,['SEA'],manual,
manual_nhl_stl,Enterprise Center,St. Louis,MO,38.6268,-90.2025,18096,NHL,['STL'],manual,
manual_nhl_tbl,Amalie Arena,Tampa,FL,27.9426,-82.4519,19092,NHL,['TBL'],manual,
manual_nhl_tor,Scotiabank Arena,Toronto,ON,43.6435,-79.3791,18819,NHL,['TOR'],manual,
manual_nhl_van,Rogers Arena,Vancouver,BC,49.2778,-123.1089,18910,NHL,['VAN'],manual,
manual_nhl_vgk,T-Mobile Arena,Las Vegas,NV,36.1028,-115.1784,17500,NHL,['VGK'],manual,
manual_nhl_wsh,Capital One Arena,Washington,DC,38.8982,-77.0209,18573,NHL,['WSH'],manual,
manual_nhl_wpg,Canada Life Centre,Winnipeg,MB,49.8928,-97.1436,15321,NHL,['WPG'],manual,
id,name,city,state,latitude,longitude,capacity,sport,team_abbrevs,source,year_opened
manual_nba_atl,State Farm Arena,Atlanta,,33.7573,-84.3963,0,NBA,['ATL'],manual,
manual_nba_bos,TD Garden,Boston,,42.3662,-71.0621,0,NBA,['BOS'],manual,
manual_nba_brk,Barclays Center,Brooklyn,,40.6826,-73.9754,0,NBA,['BRK'],manual,
manual_nba_cho,Spectrum Center,Charlotte,,35.2251,-80.8392,0,NBA,['CHO'],manual,
manual_nba_chi,United Center,Chicago,,41.8807,-87.6742,0,NBA,['CHI'],manual,
manual_nba_cle,Rocket Mortgage FieldHouse,Cleveland,,41.4965,-81.6882,0,NBA,['CLE'],manual,
manual_nba_dal,American Airlines Center,Dallas,,32.7905,-96.8103,0,NBA,['DAL'],manual,
manual_nba_den,Ball Arena,Denver,,39.7487,-105.0077,0,NBA,['DEN'],manual,
manual_nba_det,Little Caesars Arena,Detroit,,42.3411,-83.0553,0,NBA,['DET'],manual,
manual_nba_gsw,Chase Center,San Francisco,,37.768,-122.3879,0,NBA,['GSW'],manual,
manual_nba_hou,Toyota Center,Houston,,29.7508,-95.3621,0,NBA,['HOU'],manual,
manual_nba_ind,Gainbridge Fieldhouse,Indianapolis,,39.764,-86.1555,0,NBA,['IND'],manual,
manual_nba_lac,Intuit Dome,Inglewood,,33.9425,-118.3419,0,NBA,['LAC'],manual,
manual_nba_lal,Crypto.com Arena,Los Angeles,,34.043,-118.2673,0,NBA,['LAL'],manual,
manual_nba_mem,FedExForum,Memphis,,35.1382,-90.0506,0,NBA,['MEM'],manual,
manual_nba_mia,Kaseya Center,Miami,,25.7814,-80.187,0,NBA,['MIA'],manual,
manual_nba_mil,Fiserv Forum,Milwaukee,,43.0451,-87.9174,0,NBA,['MIL'],manual,
manual_nba_min,Target Center,Minneapolis,,44.9795,-93.2761,0,NBA,['MIN'],manual,
manual_nba_nop,Smoothie King Center,New Orleans,,29.949,-90.0821,0,NBA,['NOP'],manual,
manual_nba_nyk,Madison Square Garden,New York,,40.7505,-73.9934,0,NBA,['NYK'],manual,
manual_nba_okc,Paycom Center,Oklahoma City,,35.4634,-97.5151,0,NBA,['OKC'],manual,
manual_nba_orl,Kia Center,Orlando,,28.5392,-81.3839,0,NBA,['ORL'],manual,
manual_nba_phi,Wells Fargo Center,Philadelphia,,39.9012,-75.172,0,NBA,['PHI'],manual,
manual_nba_pho,Footprint Center,Phoenix,,33.4457,-112.0712,0,NBA,['PHO'],manual,
manual_nba_por,Moda Center,Portland,,45.5316,-122.6668,0,NBA,['POR'],manual,
manual_nba_sac,Golden 1 Center,Sacramento,,38.5802,-121.4997,0,NBA,['SAC'],manual,
manual_nba_sas,Frost Bank Center,San Antonio,,29.427,-98.4375,0,NBA,['SAS'],manual,
manual_nba_tor,Scotiabank Arena,Toronto,,43.6435,-79.3791,0,NBA,['TOR'],manual,
manual_nba_uta,Delta Center,Salt Lake City,,40.7683,-111.9011,0,NBA,['UTA'],manual,
manual_nba_was,Capital One Arena,Washington,,38.8982,-77.0209,0,NBA,['WAS'],manual,
manual_mlb_ari,Chase Field,Phoenix,AZ,33.4453,-112.0667,48686,MLB,['ARI'],manual,
manual_mlb_atl,Truist Park,Atlanta,GA,33.8907,-84.4678,41084,MLB,['ATL'],manual,
manual_mlb_bal,Oriole Park at Camden Yards,Baltimore,MD,39.2838,-76.6218,45971,MLB,['BAL'],manual,
manual_mlb_bos,Fenway Park,Boston,MA,42.3467,-71.0972,37755,MLB,['BOS'],manual,
manual_mlb_chc,Wrigley Field,Chicago,IL,41.9484,-87.6553,41649,MLB,['CHC'],manual,
manual_mlb_chw,Guaranteed Rate Field,Chicago,IL,41.8299,-87.6338,40615,MLB,['CHW'],manual,
manual_mlb_cin,Great American Ball Park,Cincinnati,OH,39.0979,-84.5082,42319,MLB,['CIN'],manual,
manual_mlb_cle,Progressive Field,Cleveland,OH,41.4962,-81.6852,34830,MLB,['CLE'],manual,
manual_mlb_col,Coors Field,Denver,CO,39.7559,-104.9942,50144,MLB,['COL'],manual,
manual_mlb_det,Comerica Park,Detroit,MI,42.339,-83.0485,41083,MLB,['DET'],manual,
manual_mlb_hou,Minute Maid Park,Houston,TX,29.7573,-95.3555,41168,MLB,['HOU'],manual,
manual_mlb_kcr,Kauffman Stadium,Kansas City,MO,39.0517,-94.4803,37903,MLB,['KCR'],manual,
manual_mlb_laa,Angel Stadium,Anaheim,CA,33.8003,-117.8827,45517,MLB,['LAA'],manual,
manual_mlb_lad,Dodger Stadium,Los Angeles,CA,34.0739,-118.24,56000,MLB,['LAD'],manual,
manual_mlb_mia,LoanDepot Park,Miami,FL,25.7781,-80.2196,36742,MLB,['MIA'],manual,
manual_mlb_mil,American Family Field,Milwaukee,WI,43.028,-87.9712,41900,MLB,['MIL'],manual,
manual_mlb_min,Target Field,Minneapolis,MN,44.9817,-93.2776,38544,MLB,['MIN'],manual,
manual_mlb_nym,Citi Field,New York,NY,40.7571,-73.8458,41922,MLB,['NYM'],manual,
manual_mlb_nyy,Yankee Stadium,New York,NY,40.8296,-73.9262,46537,MLB,['NYY'],manual,
manual_mlb_oak,Sutter Health Park,Sacramento,CA,38.5802,-121.5097,14014,MLB,['OAK'],manual,
manual_mlb_phi,Citizens Bank Park,Philadelphia,PA,39.9061,-75.1665,42792,MLB,['PHI'],manual,
manual_mlb_pit,PNC Park,Pittsburgh,PA,40.4469,-80.0057,38362,MLB,['PIT'],manual,
manual_mlb_sdp,Petco Park,San Diego,CA,32.7076,-117.157,40209,MLB,['SDP'],manual,
manual_mlb_sfg,Oracle Park,San Francisco,CA,37.7786,-122.3893,41265,MLB,['SFG'],manual,
manual_mlb_sea,T-Mobile Park,Seattle,WA,47.5914,-122.3325,47929,MLB,['SEA'],manual,
manual_mlb_stl,Busch Stadium,St. Louis,MO,38.6226,-90.1928,45494,MLB,['STL'],manual,
manual_mlb_tbr,Tropicana Field,St. Petersburg,FL,27.7682,-82.6534,25000,MLB,['TBR'],manual,
manual_mlb_tex,Globe Life Field,Arlington,TX,32.7473,-97.0845,40300,MLB,['TEX'],manual,
manual_mlb_tor,Rogers Centre,Toronto,ON,43.6414,-79.3894,49282,MLB,['TOR'],manual,
manual_mlb_wsn,Nationals Park,Washington,DC,38.873,-77.0074,41339,MLB,['WSN'],manual,
manual_nhl_ana,Honda Center,Anaheim,CA,33.8078,-117.8765,17174,NHL,['ANA'],manual,
manual_nhl_ari,Delta Center,Salt Lake City,UT,40.7683,-111.9011,18306,NHL,['ARI'],manual,
manual_nhl_bos,TD Garden,Boston,MA,42.3662,-71.0621,17565,NHL,['BOS'],manual,
manual_nhl_buf,KeyBank Center,Buffalo,NY,42.875,-78.8764,19070,NHL,['BUF'],manual,
manual_nhl_cgy,Scotiabank Saddledome,Calgary,AB,51.0374,-114.0519,19289,NHL,['CGY'],manual,
manual_nhl_car,PNC Arena,Raleigh,NC,35.8034,-78.722,18680,NHL,['CAR'],manual,
manual_nhl_chi,United Center,Chicago,IL,41.8807,-87.6742,19717,NHL,['CHI'],manual,
manual_nhl_col,Ball Arena,Denver,CO,39.7487,-105.0077,18007,NHL,['COL'],manual,
manual_nhl_cbj,Nationwide Arena,Columbus,OH,39.9693,-83.0061,18500,NHL,['CBJ'],manual,
manual_nhl_dal,American Airlines Center,Dallas,TX,32.7905,-96.8103,18532,NHL,['DAL'],manual,
manual_nhl_det,Little Caesars Arena,Detroit,MI,42.3411,-83.0553,19515,NHL,['DET'],manual,
manual_nhl_edm,Rogers Place,Edmonton,AB,53.5469,-113.4978,18347,NHL,['EDM'],manual,
manual_nhl_fla,Amerant Bank Arena,Sunrise,FL,26.1584,-80.3256,19250,NHL,['FLA'],manual,
manual_nhl_lak,Crypto.com Arena,Los Angeles,CA,34.043,-118.2673,18230,NHL,['LAK'],manual,
manual_nhl_min,Xcel Energy Center,St. Paul,MN,44.9448,-93.101,17954,NHL,['MIN'],manual,
manual_nhl_mtl,Bell Centre,Montreal,QC,45.4961,-73.5693,21302,NHL,['MTL'],manual,
manual_nhl_nsh,Bridgestone Arena,Nashville,TN,36.1592,-86.7785,17159,NHL,['NSH'],manual,
manual_nhl_njd,Prudential Center,Newark,NJ,40.7334,-74.1712,16514,NHL,['NJD'],manual,
manual_nhl_nyi,UBS Arena,Elmont,NY,40.7161,-73.7246,17255,NHL,['NYI'],manual,
manual_nhl_nyr,Madison Square Garden,New York,NY,40.7505,-73.9934,18006,NHL,['NYR'],manual,
manual_nhl_ott,Canadian Tire Centre,Ottawa,ON,45.2969,-75.9272,18652,NHL,['OTT'],manual,
manual_nhl_phi,Wells Fargo Center,Philadelphia,PA,39.9012,-75.172,19543,NHL,['PHI'],manual,
manual_nhl_pit,PPG Paints Arena,Pittsburgh,PA,40.4395,-79.9892,18387,NHL,['PIT'],manual,
manual_nhl_sjs,SAP Center,San Jose,CA,37.3327,-121.901,17562,NHL,['SJS'],manual,
manual_nhl_sea,Climate Pledge Arena,Seattle,WA,47.6221,-122.354,17100,NHL,['SEA'],manual,
manual_nhl_stl,Enterprise Center,St. Louis,MO,38.6268,-90.2025,18096,NHL,['STL'],manual,
manual_nhl_tbl,Amalie Arena,Tampa,FL,27.9426,-82.4519,19092,NHL,['TBL'],manual,
manual_nhl_tor,Scotiabank Arena,Toronto,ON,43.6435,-79.3791,18819,NHL,['TOR'],manual,
manual_nhl_van,Rogers Arena,Vancouver,BC,49.2778,-123.1089,18910,NHL,['VAN'],manual,
manual_nhl_vgk,T-Mobile Arena,Las Vegas,NV,36.1028,-115.1784,17500,NHL,['VGK'],manual,
manual_nhl_wsh,Capital One Arena,Washington,DC,38.8982,-77.0209,18573,NHL,['WSH'],manual,
manual_nhl_wpg,Canada Life Centre,Winnipeg,MB,49.8928,-97.1436,15321,NHL,['WPG'],manual,
manual_wnba_atl,Gateway Center Arena,College Park,GA,33.6534,-84.448,3500,WNBA,['ATL'],manual,
manual_wnba_chi,Wintrust Arena,Chicago,IL,41.8622,-87.6164,10387,WNBA,['CHI'],manual,
manual_wnba_con,Mohegan Sun Arena,Uncasville,CT,41.4946,-72.0874,10000,WNBA,['CON'],manual,
manual_wnba_dal,College Park Center,Arlington,TX,32.7298,-97.1137,7000,WNBA,['DAL'],manual,
manual_wnba_gsv,Chase Center,San Francisco,CA,37.768,-122.3879,18064,WNBA,['GSV'],manual,
manual_wnba_ind,Gainbridge Fieldhouse,Indianapolis,IN,39.764,-86.1555,17274,WNBA,['IND'],manual,
manual_wnba_lva,Michelob Ultra Arena,Las Vegas,NV,36.0929,-115.1757,12000,WNBA,['LVA'],manual,
manual_wnba_las,Crypto.com Arena,Los Angeles,CA,34.043,-118.2673,19068,WNBA,['LAS'],manual,
manual_wnba_min,Target Center,Minneapolis,MN,44.9795,-93.2761,17500,WNBA,['MIN'],manual,
manual_wnba_nyl,Barclays Center,Brooklyn,NY,40.6826,-73.9754,17732,WNBA,['NYL'],manual,
manual_wnba_phx,Footprint Center,Phoenix,AZ,33.4457,-112.0712,17000,WNBA,['PHX'],manual,
manual_wnba_sea,Climate Pledge Arena,Seattle,WA,47.6221,-122.354,17100,WNBA,['SEA'],manual,
manual_wnba_was,Entertainment & Sports Arena,Washington,DC,38.8701,-76.9728,4200,WNBA,['WAS'],manual,
manual_mls_atl,Mercedes-Benz Stadium,Atlanta,GA,33.7553,-84.4006,71000,MLS,['ATL'],manual,
manual_mls_atx,Q2 Stadium,Austin,TX,30.3876,-97.72,20738,MLS,['ATX'],manual,
manual_mls_clt,Bank of America Stadium,Charlotte,NC,35.2258,-80.8528,74867,MLS,['CLT'],manual,
manual_mls_chi,Soldier Field,Chicago,IL,41.8623,-87.6167,61500,MLS,['CHI'],manual,
manual_mls_cin,TQL Stadium,Cincinnati,OH,39.1113,-84.5212,26000,MLS,['CIN'],manual,
manual_mls_col,Dick's Sporting Goods Park,Commerce City,CO,39.8056,-104.8919,18061,MLS,['COL'],manual,
manual_mls_clb,Lower.com Field,Columbus,OH,39.9689,-83.0173,20371,MLS,['CLB'],manual,
manual_mls_dal,Toyota Stadium,Frisco,TX,33.1546,-96.8353,20500,MLS,['DAL'],manual,
manual_mls_dcu,Audi Field,Washington,DC,38.8686,-77.0128,20000,MLS,['DCU'],manual,
manual_mls_hou,Shell Energy Stadium,Houston,TX,29.7523,-95.3522,22039,MLS,['HOU'],manual,
manual_mls_lag,Dignity Health Sports Park,Carson,CA,33.8644,-118.2611,27000,MLS,['LAG'],manual,
manual_mls_lafc,BMO Stadium,Los Angeles,CA,34.0128,-118.2841,22000,MLS,['LAFC'],manual,
manual_mls_mia,Chase Stadium,Fort Lauderdale,FL,26.1902,-80.163,21550,MLS,['MIA'],manual,
manual_mls_min,Allianz Field,St. Paul,MN,44.9532,-93.1653,19400,MLS,['MIN'],manual,
manual_mls_mtl,Stade Saputo,Montreal,QC,45.5628,-73.553,19619,MLS,['MTL'],manual,
manual_mls_nsh,Geodis Park,Nashville,TN,36.1303,-86.7663,30000,MLS,['NSH'],manual,
manual_mls_ner,Gillette Stadium,Foxborough,MA,42.0909,-71.2643,65878,MLS,['NER'],manual,
manual_mls_nyc,Yankee Stadium,New York,NY,40.8296,-73.9262,46537,MLS,['NYC'],manual,
manual_mls_rbny,Red Bull Arena,Harrison,NJ,40.7368,-74.1503,25000,MLS,['RBNY'],manual,
manual_mls_orl,Inter&Co Stadium,Orlando,FL,28.5411,-81.3899,25500,MLS,['ORL'],manual,
manual_mls_phi,Subaru Park,Chester,PA,39.8328,-75.3789,18500,MLS,['PHI'],manual,
manual_mls_por,Providence Park,Portland,OR,45.5217,-122.6917,25218,MLS,['POR'],manual,
manual_mls_rsl,America First Field,Sandy,UT,40.5828,-111.8933,20213,MLS,['RSL'],manual,
manual_mls_sje,PayPal Park,San Jose,CA,37.3513,-121.9253,18000,MLS,['SJE'],manual,
manual_mls_sea,Lumen Field,Seattle,WA,47.5952,-122.3316,68740,MLS,['SEA'],manual,
manual_mls_skc,Children's Mercy Park,Kansas City,KS,39.1218,-94.8234,18467,MLS,['SKC'],manual,
manual_mls_stl,CityPark,St. Louis,MO,38.6322,-90.2094,22500,MLS,['STL'],manual,
manual_mls_tor,BMO Field,Toronto,ON,43.6332,-79.4186,30000,MLS,['TOR'],manual,
manual_mls_van,BC Place,Vancouver,BC,49.2768,-123.1118,54320,MLS,['VAN'],manual,
manual_mls_sdg,Snapdragon Stadium,San Diego,CA,32.7839,-117.1224,35000,MLS,['SDG'],manual,
manual_nwsl_ang,BMO Stadium,Los Angeles,CA,34.0128,-118.2841,22000,NWSL,['ANG'],manual,
manual_nwsl_bay,PayPal Park,San Jose,CA,37.3513,-121.9253,18000,NWSL,['BAY'],manual,
manual_nwsl_chi,SeatGeek Stadium,Chicago,IL,41.6462,-87.7304,20000,NWSL,['CHI'],manual,
manual_nwsl_hou,Shell Energy Stadium,Houston,TX,29.7523,-95.3522,22039,NWSL,['HOU'],manual,
manual_nwsl_kcc,CPKC Stadium,Kansas City,KS,39.0851,-94.5582,11500,NWSL,['KCC'],manual,
manual_nwsl_njy,Red Bull Arena,Harrison,NJ,40.7368,-74.1503,25000,NWSL,['NJY'],manual,
manual_nwsl_ncc,WakeMed Soccer Park,Cary,NC,35.8589,-78.7989,10000,NWSL,['NCC'],manual,
manual_nwsl_orl,Inter&Co Stadium,Orlando,FL,28.5411,-81.3899,25500,NWSL,['ORL'],manual,
manual_nwsl_por,Providence Park,Portland,OR,45.5217,-122.6917,25218,NWSL,['POR'],manual,
manual_nwsl_rgn,Lumen Field,Seattle,WA,47.5952,-122.3316,68740,NWSL,['RGN'],manual,
manual_nwsl_sdw,Snapdragon Stadium,San Diego,CA,32.7839,-117.1224,35000,NWSL,['SDW'],manual,
manual_nwsl_uta,America First Field,Sandy,UT,40.5828,-111.8933,20213,NWSL,['UTA'],manual,
manual_nwsl_wsh,Audi Field,Washington,DC,38.8686,-77.0128,20000,NWSL,['WSH'],manual,
manual_nfl_ari,State Farm Stadium,Glendale,AZ,33.5276,-112.2626,63400,NFL,['ARI'],manual,
manual_nfl_atl,Mercedes-Benz Stadium,Atlanta,GA,33.7553,-84.4006,71000,NFL,['ATL'],manual,
manual_nfl_bal,M&T Bank Stadium,Baltimore,MD,39.278,-76.6227,71008,NFL,['BAL'],manual,
manual_nfl_buf,Highmark Stadium,Orchard Park,NY,42.7738,-78.787,71608,NFL,['BUF'],manual,
manual_nfl_car,Bank of America Stadium,Charlotte,NC,35.2258,-80.8528,74867,NFL,['CAR'],manual,
manual_nfl_chi,Soldier Field,Chicago,IL,41.8623,-87.6167,61500,NFL,['CHI'],manual,
manual_nfl_cin,Paycor Stadium,Cincinnati,OH,39.0954,-84.516,65515,NFL,['CIN'],manual,
manual_nfl_cle,Cleveland Browns Stadium,Cleveland,OH,41.5061,-81.6995,67431,NFL,['CLE'],manual,
manual_nfl_dal,AT&T Stadium,Arlington,TX,32.748,-97.0928,80000,NFL,['DAL'],manual,
manual_nfl_den,Empower Field at Mile High,Denver,CO,39.7439,-105.0201,76125,NFL,['DEN'],manual,
manual_nfl_det,Ford Field,Detroit,MI,42.34,-83.0456,65000,NFL,['DET'],manual,
manual_nfl_gb,Lambeau Field,Green Bay,WI,44.5013,-88.0622,81435,NFL,['GB'],manual,
manual_nfl_hou,NRG Stadium,Houston,TX,29.6847,-95.4107,72220,NFL,['HOU'],manual,
manual_nfl_ind,Lucas Oil Stadium,Indianapolis,IN,39.7601,-86.1639,67000,NFL,['IND'],manual,
manual_nfl_jax,EverBank Stadium,Jacksonville,FL,30.3239,-81.6373,67814,NFL,['JAX'],manual,
manual_nfl_kc,GEHA Field at Arrowhead Stadium,Kansas City,MO,39.0489,-94.4839,76416,NFL,['KC'],manual,
manual_nfl_lv,Allegiant Stadium,Las Vegas,NV,36.0909,-115.1833,65000,NFL,['LV'],manual,
manual_nfl_lac,SoFi Stadium,Inglewood,CA,33.9535,-118.3392,70240,NFL,['LAC'],manual,
manual_nfl_lar,SoFi Stadium,Inglewood,CA,33.9535,-118.3392,70240,NFL,['LAR'],manual,
manual_nfl_mia,Hard Rock Stadium,Miami Gardens,FL,25.958,-80.2389,65326,NFL,['MIA'],manual,
manual_nfl_min,U.S. Bank Stadium,Minneapolis,MN,44.9737,-93.2577,66655,NFL,['MIN'],manual,
manual_nfl_ne,Gillette Stadium,Foxborough,MA,42.0909,-71.2643,65878,NFL,['NE'],manual,
manual_nfl_no,Caesars Superdome,New Orleans,LA,29.9511,-90.0812,73208,NFL,['NO'],manual,
manual_nfl_nyg,MetLife Stadium,East Rutherford,NJ,40.8128,-74.0742,82500,NFL,['NYG'],manual,
manual_nfl_nyj,MetLife Stadium,East Rutherford,NJ,40.8128,-74.0742,82500,NFL,['NYJ'],manual,
manual_nfl_phi,Lincoln Financial Field,Philadelphia,PA,39.9008,-75.1674,69176,NFL,['PHI'],manual,
manual_nfl_pit,Acrisure Stadium,Pittsburgh,PA,40.4468,-80.0158,68400,NFL,['PIT'],manual,
manual_nfl_sf,Levi's Stadium,Santa Clara,CA,37.4032,-121.9698,68500,NFL,['SF'],manual,
manual_nfl_sea,Lumen Field,Seattle,WA,47.5952,-122.3316,68740,NFL,['SEA'],manual,
manual_nfl_tb,Raymond James Stadium,Tampa,FL,27.9759,-82.5033,65618,NFL,['TB'],manual,
manual_nfl_ten,Nissan Stadium,Nashville,TN,36.1665,-86.7713,69143,NFL,['TEN'],manual,
manual_nfl_was,Northwest Stadium,Landover,MD,38.9076,-76.8645,67617,NFL,['WAS'],manual,
1 id name city state latitude longitude capacity sport team_abbrevs source year_opened
2 manual_nba_atl State Farm Arena Atlanta 33.7573 -84.3963 0 NBA ['ATL'] manual
3 manual_nba_bos TD Garden Boston 42.3662 -71.0621 0 NBA ['BOS'] manual
4 manual_nba_brk Barclays Center Brooklyn 40.6826 -73.9754 0 NBA ['BRK'] manual
5 manual_nba_cho Spectrum Center Charlotte 35.2251 -80.8392 0 NBA ['CHO'] manual
6 manual_nba_chi United Center Chicago 41.8807 -87.6742 0 NBA ['CHI'] manual
7 manual_nba_cle Rocket Mortgage FieldHouse Cleveland 41.4965 -81.6882 0 NBA ['CLE'] manual
8 manual_nba_dal American Airlines Center Dallas 32.7905 -96.8103 0 NBA ['DAL'] manual
9 manual_nba_den Ball Arena Denver 39.7487 -105.0077 0 NBA ['DEN'] manual
10 manual_nba_det Little Caesars Arena Detroit 42.3411 -83.0553 0 NBA ['DET'] manual
11 manual_nba_gsw Chase Center San Francisco 37.768 -122.3879 0 NBA ['GSW'] manual
12 manual_nba_hou Toyota Center Houston 29.7508 -95.3621 0 NBA ['HOU'] manual
13 manual_nba_ind Gainbridge Fieldhouse Indianapolis 39.764 -86.1555 0 NBA ['IND'] manual
14 manual_nba_lac Intuit Dome Inglewood 33.9425 -118.3419 0 NBA ['LAC'] manual
15 manual_nba_lal Crypto.com Arena Los Angeles 34.043 -118.2673 0 NBA ['LAL'] manual
16 manual_nba_mem FedExForum Memphis 35.1382 -90.0506 0 NBA ['MEM'] manual
17 manual_nba_mia Kaseya Center Miami 25.7814 -80.187 0 NBA ['MIA'] manual
18 manual_nba_mil Fiserv Forum Milwaukee 43.0451 -87.9174 0 NBA ['MIL'] manual
19 manual_nba_min Target Center Minneapolis 44.9795 -93.2761 0 NBA ['MIN'] manual
20 manual_nba_nop Smoothie King Center New Orleans 29.949 -90.0821 0 NBA ['NOP'] manual
21 manual_nba_nyk Madison Square Garden New York 40.7505 -73.9934 0 NBA ['NYK'] manual
22 manual_nba_okc Paycom Center Oklahoma City 35.4634 -97.5151 0 NBA ['OKC'] manual
23 manual_nba_orl Kia Center Orlando 28.5392 -81.3839 0 NBA ['ORL'] manual
24 manual_nba_phi Wells Fargo Center Philadelphia 39.9012 -75.172 0 NBA ['PHI'] manual
25 manual_nba_pho Footprint Center Phoenix 33.4457 -112.0712 0 NBA ['PHO'] manual
26 manual_nba_por Moda Center Portland 45.5316 -122.6668 0 NBA ['POR'] manual
27 manual_nba_sac Golden 1 Center Sacramento 38.5802 -121.4997 0 NBA ['SAC'] manual
28 manual_nba_sas Frost Bank Center San Antonio 29.427 -98.4375 0 NBA ['SAS'] manual
29 manual_nba_tor Scotiabank Arena Toronto 43.6435 -79.3791 0 NBA ['TOR'] manual
30 manual_nba_uta Delta Center Salt Lake City 40.7683 -111.9011 0 NBA ['UTA'] manual
31 manual_nba_was Capital One Arena Washington 38.8982 -77.0209 0 NBA ['WAS'] manual
32 manual_mlb_ari Chase Field Phoenix AZ 33.4453 -112.0667 48686 MLB ['ARI'] manual
33 manual_mlb_atl Truist Park Atlanta GA 33.8907 -84.4678 41084 MLB ['ATL'] manual
34 manual_mlb_bal Oriole Park at Camden Yards Baltimore MD 39.2838 -76.6218 45971 MLB ['BAL'] manual
35 manual_mlb_bos Fenway Park Boston MA 42.3467 -71.0972 37755 MLB ['BOS'] manual
36 manual_mlb_chc Wrigley Field Chicago IL 41.9484 -87.6553 41649 MLB ['CHC'] manual
37 manual_mlb_chw Guaranteed Rate Field Chicago IL 41.8299 -87.6338 40615 MLB ['CHW'] manual
38 manual_mlb_cin Great American Ball Park Cincinnati OH 39.0979 -84.5082 42319 MLB ['CIN'] manual
39 manual_mlb_cle Progressive Field Cleveland OH 41.4962 -81.6852 34830 MLB ['CLE'] manual
40 manual_mlb_col Coors Field Denver CO 39.7559 -104.9942 50144 MLB ['COL'] manual
41 manual_mlb_det Comerica Park Detroit MI 42.339 -83.0485 41083 MLB ['DET'] manual
42 manual_mlb_hou Minute Maid Park Houston TX 29.7573 -95.3555 41168 MLB ['HOU'] manual
43 manual_mlb_kcr Kauffman Stadium Kansas City MO 39.0517 -94.4803 37903 MLB ['KCR'] manual
44 manual_mlb_laa Angel Stadium Anaheim CA 33.8003 -117.8827 45517 MLB ['LAA'] manual
45 manual_mlb_lad Dodger Stadium Los Angeles CA 34.0739 -118.24 56000 MLB ['LAD'] manual
46 manual_mlb_mia LoanDepot Park Miami FL 25.7781 -80.2196 36742 MLB ['MIA'] manual
47 manual_mlb_mil American Family Field Milwaukee WI 43.028 -87.9712 41900 MLB ['MIL'] manual
48 manual_mlb_min Target Field Minneapolis MN 44.9817 -93.2776 38544 MLB ['MIN'] manual
49 manual_mlb_nym Citi Field New York NY 40.7571 -73.8458 41922 MLB ['NYM'] manual
50 manual_mlb_nyy Yankee Stadium New York NY 40.8296 -73.9262 46537 MLB ['NYY'] manual
51 manual_mlb_oak Sutter Health Park Sacramento CA 38.5802 -121.5097 14014 MLB ['OAK'] manual
52 manual_mlb_phi Citizens Bank Park Philadelphia PA 39.9061 -75.1665 42792 MLB ['PHI'] manual
53 manual_mlb_pit PNC Park Pittsburgh PA 40.4469 -80.0057 38362 MLB ['PIT'] manual
54 manual_mlb_sdp Petco Park San Diego CA 32.7076 -117.157 40209 MLB ['SDP'] manual
55 manual_mlb_sfg Oracle Park San Francisco CA 37.7786 -122.3893 41265 MLB ['SFG'] manual
56 manual_mlb_sea T-Mobile Park Seattle WA 47.5914 -122.3325 47929 MLB ['SEA'] manual
57 manual_mlb_stl Busch Stadium St. Louis MO 38.6226 -90.1928 45494 MLB ['STL'] manual
58 manual_mlb_tbr Tropicana Field St. Petersburg FL 27.7682 -82.6534 25000 MLB ['TBR'] manual
59 manual_mlb_tex Globe Life Field Arlington TX 32.7473 -97.0845 40300 MLB ['TEX'] manual
60 manual_mlb_tor Rogers Centre Toronto ON 43.6414 -79.3894 49282 MLB ['TOR'] manual
61 manual_mlb_wsn Nationals Park Washington DC 38.873 -77.0074 41339 MLB ['WSN'] manual
62 manual_nhl_ana Honda Center Anaheim CA 33.8078 -117.8765 17174 NHL ['ANA'] manual
63 manual_nhl_ari Delta Center Salt Lake City UT 40.7683 -111.9011 18306 NHL ['ARI'] manual
64 manual_nhl_bos TD Garden Boston MA 42.3662 -71.0621 17565 NHL ['BOS'] manual
65 manual_nhl_buf KeyBank Center Buffalo NY 42.875 -78.8764 19070 NHL ['BUF'] manual
66 manual_nhl_cgy Scotiabank Saddledome Calgary AB 51.0374 -114.0519 19289 NHL ['CGY'] manual
67 manual_nhl_car PNC Arena Raleigh NC 35.8034 -78.722 18680 NHL ['CAR'] manual
68 manual_nhl_chi United Center Chicago IL 41.8807 -87.6742 19717 NHL ['CHI'] manual
69 manual_nhl_col Ball Arena Denver CO 39.7487 -105.0077 18007 NHL ['COL'] manual
70 manual_nhl_cbj Nationwide Arena Columbus OH 39.9693 -83.0061 18500 NHL ['CBJ'] manual
71 manual_nhl_dal American Airlines Center Dallas TX 32.7905 -96.8103 18532 NHL ['DAL'] manual
72 manual_nhl_det Little Caesars Arena Detroit MI 42.3411 -83.0553 19515 NHL ['DET'] manual
73 manual_nhl_edm Rogers Place Edmonton AB 53.5469 -113.4978 18347 NHL ['EDM'] manual
74 manual_nhl_fla Amerant Bank Arena Sunrise FL 26.1584 -80.3256 19250 NHL ['FLA'] manual
75 manual_nhl_lak Crypto.com Arena Los Angeles CA 34.043 -118.2673 18230 NHL ['LAK'] manual
76 manual_nhl_min Xcel Energy Center St. Paul MN 44.9448 -93.101 17954 NHL ['MIN'] manual
77 manual_nhl_mtl Bell Centre Montreal QC 45.4961 -73.5693 21302 NHL ['MTL'] manual
78 manual_nhl_nsh Bridgestone Arena Nashville TN 36.1592 -86.7785 17159 NHL ['NSH'] manual
79 manual_nhl_njd Prudential Center Newark NJ 40.7334 -74.1712 16514 NHL ['NJD'] manual
80 manual_nhl_nyi UBS Arena Elmont NY 40.7161 -73.7246 17255 NHL ['NYI'] manual
81 manual_nhl_nyr Madison Square Garden New York NY 40.7505 -73.9934 18006 NHL ['NYR'] manual
82 manual_nhl_ott Canadian Tire Centre Ottawa ON 45.2969 -75.9272 18652 NHL ['OTT'] manual
83 manual_nhl_phi Wells Fargo Center Philadelphia PA 39.9012 -75.172 19543 NHL ['PHI'] manual
84 manual_nhl_pit PPG Paints Arena Pittsburgh PA 40.4395 -79.9892 18387 NHL ['PIT'] manual
85 manual_nhl_sjs SAP Center San Jose CA 37.3327 -121.901 17562 NHL ['SJS'] manual
86 manual_nhl_sea Climate Pledge Arena Seattle WA 47.6221 -122.354 17100 NHL ['SEA'] manual
87 manual_nhl_stl Enterprise Center St. Louis MO 38.6268 -90.2025 18096 NHL ['STL'] manual
88 manual_nhl_tbl Amalie Arena Tampa FL 27.9426 -82.4519 19092 NHL ['TBL'] manual
89 manual_nhl_tor Scotiabank Arena Toronto ON 43.6435 -79.3791 18819 NHL ['TOR'] manual
90 manual_nhl_van Rogers Arena Vancouver BC 49.2778 -123.1089 18910 NHL ['VAN'] manual
91 manual_nhl_vgk T-Mobile Arena Las Vegas NV 36.1028 -115.1784 17500 NHL ['VGK'] manual
92 manual_nhl_wsh Capital One Arena Washington DC 38.8982 -77.0209 18573 NHL ['WSH'] manual
93 manual_nhl_wpg Canada Life Centre Winnipeg MB 49.8928 -97.1436 15321 NHL ['WPG'] manual
94 manual_wnba_atl Gateway Center Arena College Park GA 33.6534 -84.448 3500 WNBA ['ATL'] manual
95 manual_wnba_chi Wintrust Arena Chicago IL 41.8622 -87.6164 10387 WNBA ['CHI'] manual
96 manual_wnba_con Mohegan Sun Arena Uncasville CT 41.4946 -72.0874 10000 WNBA ['CON'] manual
97 manual_wnba_dal College Park Center Arlington TX 32.7298 -97.1137 7000 WNBA ['DAL'] manual
98 manual_wnba_gsv Chase Center San Francisco CA 37.768 -122.3879 18064 WNBA ['GSV'] manual
99 manual_wnba_ind Gainbridge Fieldhouse Indianapolis IN 39.764 -86.1555 17274 WNBA ['IND'] manual
100 manual_wnba_lva Michelob Ultra Arena Las Vegas NV 36.0929 -115.1757 12000 WNBA ['LVA'] manual
101 manual_wnba_las Crypto.com Arena Los Angeles CA 34.043 -118.2673 19068 WNBA ['LAS'] manual
102 manual_wnba_min Target Center Minneapolis MN 44.9795 -93.2761 17500 WNBA ['MIN'] manual
103 manual_wnba_nyl Barclays Center Brooklyn NY 40.6826 -73.9754 17732 WNBA ['NYL'] manual
104 manual_wnba_phx Footprint Center Phoenix AZ 33.4457 -112.0712 17000 WNBA ['PHX'] manual
105 manual_wnba_sea Climate Pledge Arena Seattle WA 47.6221 -122.354 17100 WNBA ['SEA'] manual
106 manual_wnba_was Entertainment & Sports Arena Washington DC 38.8701 -76.9728 4200 WNBA ['WAS'] manual
107 manual_mls_atl Mercedes-Benz Stadium Atlanta GA 33.7553 -84.4006 71000 MLS ['ATL'] manual
108 manual_mls_atx Q2 Stadium Austin TX 30.3876 -97.72 20738 MLS ['ATX'] manual
109 manual_mls_clt Bank of America Stadium Charlotte NC 35.2258 -80.8528 74867 MLS ['CLT'] manual
110 manual_mls_chi Soldier Field Chicago IL 41.8623 -87.6167 61500 MLS ['CHI'] manual
111 manual_mls_cin TQL Stadium Cincinnati OH 39.1113 -84.5212 26000 MLS ['CIN'] manual
112 manual_mls_col Dick's Sporting Goods Park Commerce City CO 39.8056 -104.8919 18061 MLS ['COL'] manual
113 manual_mls_clb Lower.com Field Columbus OH 39.9689 -83.0173 20371 MLS ['CLB'] manual
114 manual_mls_dal Toyota Stadium Frisco TX 33.1546 -96.8353 20500 MLS ['DAL'] manual
115 manual_mls_dcu Audi Field Washington DC 38.8686 -77.0128 20000 MLS ['DCU'] manual
116 manual_mls_hou Shell Energy Stadium Houston TX 29.7523 -95.3522 22039 MLS ['HOU'] manual
117 manual_mls_lag Dignity Health Sports Park Carson CA 33.8644 -118.2611 27000 MLS ['LAG'] manual
118 manual_mls_lafc BMO Stadium Los Angeles CA 34.0128 -118.2841 22000 MLS ['LAFC'] manual
119 manual_mls_mia Chase Stadium Fort Lauderdale FL 26.1902 -80.163 21550 MLS ['MIA'] manual
120 manual_mls_min Allianz Field St. Paul MN 44.9532 -93.1653 19400 MLS ['MIN'] manual
121 manual_mls_mtl Stade Saputo Montreal QC 45.5628 -73.553 19619 MLS ['MTL'] manual
122 manual_mls_nsh Geodis Park Nashville TN 36.1303 -86.7663 30000 MLS ['NSH'] manual
123 manual_mls_ner Gillette Stadium Foxborough MA 42.0909 -71.2643 65878 MLS ['NER'] manual
124 manual_mls_nyc Yankee Stadium New York NY 40.8296 -73.9262 46537 MLS ['NYC'] manual
125 manual_mls_rbny Red Bull Arena Harrison NJ 40.7368 -74.1503 25000 MLS ['RBNY'] manual
126 manual_mls_orl Inter&Co Stadium Orlando FL 28.5411 -81.3899 25500 MLS ['ORL'] manual
127 manual_mls_phi Subaru Park Chester PA 39.8328 -75.3789 18500 MLS ['PHI'] manual
128 manual_mls_por Providence Park Portland OR 45.5217 -122.6917 25218 MLS ['POR'] manual
129 manual_mls_rsl America First Field Sandy UT 40.5828 -111.8933 20213 MLS ['RSL'] manual
130 manual_mls_sje PayPal Park San Jose CA 37.3513 -121.9253 18000 MLS ['SJE'] manual
131 manual_mls_sea Lumen Field Seattle WA 47.5952 -122.3316 68740 MLS ['SEA'] manual
132 manual_mls_skc Children's Mercy Park Kansas City KS 39.1218 -94.8234 18467 MLS ['SKC'] manual
133 manual_mls_stl CityPark St. Louis MO 38.6322 -90.2094 22500 MLS ['STL'] manual
134 manual_mls_tor BMO Field Toronto ON 43.6332 -79.4186 30000 MLS ['TOR'] manual
135 manual_mls_van BC Place Vancouver BC 49.2768 -123.1118 54320 MLS ['VAN'] manual
136 manual_mls_sdg Snapdragon Stadium San Diego CA 32.7839 -117.1224 35000 MLS ['SDG'] manual
137 manual_nwsl_ang BMO Stadium Los Angeles CA 34.0128 -118.2841 22000 NWSL ['ANG'] manual
138 manual_nwsl_bay PayPal Park San Jose CA 37.3513 -121.9253 18000 NWSL ['BAY'] manual
139 manual_nwsl_chi SeatGeek Stadium Chicago IL 41.6462 -87.7304 20000 NWSL ['CHI'] manual
140 manual_nwsl_hou Shell Energy Stadium Houston TX 29.7523 -95.3522 22039 NWSL ['HOU'] manual
141 manual_nwsl_kcc CPKC Stadium Kansas City KS 39.0851 -94.5582 11500 NWSL ['KCC'] manual
142 manual_nwsl_njy Red Bull Arena Harrison NJ 40.7368 -74.1503 25000 NWSL ['NJY'] manual
143 manual_nwsl_ncc WakeMed Soccer Park Cary NC 35.8589 -78.7989 10000 NWSL ['NCC'] manual
144 manual_nwsl_orl Inter&Co Stadium Orlando FL 28.5411 -81.3899 25500 NWSL ['ORL'] manual
145 manual_nwsl_por Providence Park Portland OR 45.5217 -122.6917 25218 NWSL ['POR'] manual
146 manual_nwsl_rgn Lumen Field Seattle WA 47.5952 -122.3316 68740 NWSL ['RGN'] manual
147 manual_nwsl_sdw Snapdragon Stadium San Diego CA 32.7839 -117.1224 35000 NWSL ['SDW'] manual
148 manual_nwsl_uta America First Field Sandy UT 40.5828 -111.8933 20213 NWSL ['UTA'] manual
149 manual_nwsl_wsh Audi Field Washington DC 38.8686 -77.0128 20000 NWSL ['WSH'] manual
150 manual_nfl_ari State Farm Stadium Glendale AZ 33.5276 -112.2626 63400 NFL ['ARI'] manual
151 manual_nfl_atl Mercedes-Benz Stadium Atlanta GA 33.7553 -84.4006 71000 NFL ['ATL'] manual
152 manual_nfl_bal M&T Bank Stadium Baltimore MD 39.278 -76.6227 71008 NFL ['BAL'] manual
153 manual_nfl_buf Highmark Stadium Orchard Park NY 42.7738 -78.787 71608 NFL ['BUF'] manual
154 manual_nfl_car Bank of America Stadium Charlotte NC 35.2258 -80.8528 74867 NFL ['CAR'] manual
155 manual_nfl_chi Soldier Field Chicago IL 41.8623 -87.6167 61500 NFL ['CHI'] manual
156 manual_nfl_cin Paycor Stadium Cincinnati OH 39.0954 -84.516 65515 NFL ['CIN'] manual
157 manual_nfl_cle Cleveland Browns Stadium Cleveland OH 41.5061 -81.6995 67431 NFL ['CLE'] manual
158 manual_nfl_dal AT&T Stadium Arlington TX 32.748 -97.0928 80000 NFL ['DAL'] manual
159 manual_nfl_den Empower Field at Mile High Denver CO 39.7439 -105.0201 76125 NFL ['DEN'] manual
160 manual_nfl_det Ford Field Detroit MI 42.34 -83.0456 65000 NFL ['DET'] manual
161 manual_nfl_gb Lambeau Field Green Bay WI 44.5013 -88.0622 81435 NFL ['GB'] manual
162 manual_nfl_hou NRG Stadium Houston TX 29.6847 -95.4107 72220 NFL ['HOU'] manual
163 manual_nfl_ind Lucas Oil Stadium Indianapolis IN 39.7601 -86.1639 67000 NFL ['IND'] manual
164 manual_nfl_jax EverBank Stadium Jacksonville FL 30.3239 -81.6373 67814 NFL ['JAX'] manual
165 manual_nfl_kc GEHA Field at Arrowhead Stadium Kansas City MO 39.0489 -94.4839 76416 NFL ['KC'] manual
166 manual_nfl_lv Allegiant Stadium Las Vegas NV 36.0909 -115.1833 65000 NFL ['LV'] manual
167 manual_nfl_lac SoFi Stadium Inglewood CA 33.9535 -118.3392 70240 NFL ['LAC'] manual
168 manual_nfl_lar SoFi Stadium Inglewood CA 33.9535 -118.3392 70240 NFL ['LAR'] manual
169 manual_nfl_mia Hard Rock Stadium Miami Gardens FL 25.958 -80.2389 65326 NFL ['MIA'] manual
170 manual_nfl_min U.S. Bank Stadium Minneapolis MN 44.9737 -93.2577 66655 NFL ['MIN'] manual
171 manual_nfl_ne Gillette Stadium Foxborough MA 42.0909 -71.2643 65878 NFL ['NE'] manual
172 manual_nfl_no Caesars Superdome New Orleans LA 29.9511 -90.0812 73208 NFL ['NO'] manual
173 manual_nfl_nyg MetLife Stadium East Rutherford NJ 40.8128 -74.0742 82500 NFL ['NYG'] manual
174 manual_nfl_nyj MetLife Stadium East Rutherford NJ 40.8128 -74.0742 82500 NFL ['NYJ'] manual
175 manual_nfl_phi Lincoln Financial Field Philadelphia PA 39.9008 -75.1674 69176 NFL ['PHI'] manual
176 manual_nfl_pit Acrisure Stadium Pittsburgh PA 40.4468 -80.0158 68400 NFL ['PIT'] manual
177 manual_nfl_sf Levi's Stadium Santa Clara CA 37.4032 -121.9698 68500 NFL ['SF'] manual
178 manual_nfl_sea Lumen Field Seattle WA 47.5952 -122.3316 68740 NFL ['SEA'] manual
179 manual_nfl_tb Raymond James Stadium Tampa FL 27.9759 -82.5033 65618 NFL ['TB'] manual
180 manual_nfl_ten Nissan Stadium Nashville TN 36.1665 -86.7713 69143 NFL ['TEN'] manual
181 manual_nfl_was Northwest Stadium Landover MD 38.9076 -76.8645 67617 NFL ['WAS'] manual

File diff suppressed because it is too large Load Diff

View File

@@ -1286,5 +1286,789 @@
"WPG"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_gateway_center_arena",
"name": "Gateway Center Arena",
"city": "College Park",
"state": "GA",
"latitude": 33.6534,
"longitude": -84.448,
"capacity": 3500,
"sport": "WNBA",
"primary_team_abbrevs": [
"ATL"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_wintrust_arena",
"name": "Wintrust Arena",
"city": "Chicago",
"state": "IL",
"latitude": 41.8622,
"longitude": -87.6164,
"capacity": 10387,
"sport": "WNBA",
"primary_team_abbrevs": [
"CHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_mohegan_sun_arena",
"name": "Mohegan Sun Arena",
"city": "Uncasville",
"state": "CT",
"latitude": 41.4946,
"longitude": -72.0874,
"capacity": 10000,
"sport": "WNBA",
"primary_team_abbrevs": [
"CON"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_college_park_center",
"name": "College Park Center",
"city": "Arlington",
"state": "TX",
"latitude": 32.7298,
"longitude": -97.1137,
"capacity": 7000,
"sport": "WNBA",
"primary_team_abbrevs": [
"DAL"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_chase_center",
"name": "Chase Center",
"city": "San Francisco",
"state": "CA",
"latitude": 37.768,
"longitude": -122.3879,
"capacity": 18064,
"sport": "WNBA",
"primary_team_abbrevs": [
"GSV"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_gainbridge_fieldhouse",
"name": "Gainbridge Fieldhouse",
"city": "Indianapolis",
"state": "IN",
"latitude": 39.764,
"longitude": -86.1555,
"capacity": 17274,
"sport": "WNBA",
"primary_team_abbrevs": [
"IND"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_michelob_ultra_arena",
"name": "Michelob Ultra Arena",
"city": "Las Vegas",
"state": "NV",
"latitude": 36.0929,
"longitude": -115.1757,
"capacity": 12000,
"sport": "WNBA",
"primary_team_abbrevs": [
"LVA"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_cryptocom_arena",
"name": "Crypto.com Arena",
"city": "Los Angeles",
"state": "CA",
"latitude": 34.043,
"longitude": -118.2673,
"capacity": 19068,
"sport": "WNBA",
"primary_team_abbrevs": [
"LAS"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_target_center",
"name": "Target Center",
"city": "Minneapolis",
"state": "MN",
"latitude": 44.9795,
"longitude": -93.2761,
"capacity": 17500,
"sport": "WNBA",
"primary_team_abbrevs": [
"MIN"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_barclays_center",
"name": "Barclays Center",
"city": "Brooklyn",
"state": "NY",
"latitude": 40.6826,
"longitude": -73.9754,
"capacity": 17732,
"sport": "WNBA",
"primary_team_abbrevs": [
"NYL"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_footprint_center",
"name": "Footprint Center",
"city": "Phoenix",
"state": "AZ",
"latitude": 33.4457,
"longitude": -112.0712,
"capacity": 17000,
"sport": "WNBA",
"primary_team_abbrevs": [
"PHX"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_climate_pledge_arena",
"name": "Climate Pledge Arena",
"city": "Seattle",
"state": "WA",
"latitude": 47.6221,
"longitude": -122.354,
"capacity": 17100,
"sport": "WNBA",
"primary_team_abbrevs": [
"SEA"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_entertainment_sports_arena",
"name": "Entertainment & Sports Arena",
"city": "Washington",
"state": "DC",
"latitude": 38.8701,
"longitude": -76.9728,
"capacity": 4200,
"sport": "WNBA",
"primary_team_abbrevs": [
"WAS"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_mercedesbenz_stadium",
"name": "Mercedes-Benz Stadium",
"city": "Atlanta",
"state": "GA",
"latitude": 33.7553,
"longitude": -84.4006,
"capacity": 71000,
"sport": "MLS",
"primary_team_abbrevs": [
"ATL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_q2_stadium",
"name": "Q2 Stadium",
"city": "Austin",
"state": "TX",
"latitude": 30.3876,
"longitude": -97.72,
"capacity": 20738,
"sport": "MLS",
"primary_team_abbrevs": [
"ATX"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bank_of_america_stadium",
"name": "Bank of America Stadium",
"city": "Charlotte",
"state": "NC",
"latitude": 35.2258,
"longitude": -80.8528,
"capacity": 74867,
"sport": "MLS",
"primary_team_abbrevs": [
"CLT"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_soldier_field",
"name": "Soldier Field",
"city": "Chicago",
"state": "IL",
"latitude": 41.8623,
"longitude": -87.6167,
"capacity": 61500,
"sport": "MLS",
"primary_team_abbrevs": [
"CHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_tql_stadium",
"name": "TQL Stadium",
"city": "Cincinnati",
"state": "OH",
"latitude": 39.1113,
"longitude": -84.5212,
"capacity": 26000,
"sport": "MLS",
"primary_team_abbrevs": [
"CIN"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_dicks_sporting_goods_park",
"name": "Dick's Sporting Goods Park",
"city": "Commerce City",
"state": "CO",
"latitude": 39.8056,
"longitude": -104.8919,
"capacity": 18061,
"sport": "MLS",
"primary_team_abbrevs": [
"COL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_lowercom_field",
"name": "Lower.com Field",
"city": "Columbus",
"state": "OH",
"latitude": 39.9689,
"longitude": -83.0173,
"capacity": 20371,
"sport": "MLS",
"primary_team_abbrevs": [
"CLB"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_toyota_stadium",
"name": "Toyota Stadium",
"city": "Frisco",
"state": "TX",
"latitude": 33.1546,
"longitude": -96.8353,
"capacity": 20500,
"sport": "MLS",
"primary_team_abbrevs": [
"DAL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_audi_field",
"name": "Audi Field",
"city": "Washington",
"state": "DC",
"latitude": 38.8686,
"longitude": -77.0128,
"capacity": 20000,
"sport": "MLS",
"primary_team_abbrevs": [
"DCU"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_shell_energy_stadium",
"name": "Shell Energy Stadium",
"city": "Houston",
"state": "TX",
"latitude": 29.7523,
"longitude": -95.3522,
"capacity": 22039,
"sport": "MLS",
"primary_team_abbrevs": [
"HOU"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_dignity_health_sports_park",
"name": "Dignity Health Sports Park",
"city": "Carson",
"state": "CA",
"latitude": 33.8644,
"longitude": -118.2611,
"capacity": 27000,
"sport": "MLS",
"primary_team_abbrevs": [
"LAG"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bmo_stadium",
"name": "BMO Stadium",
"city": "Los Angeles",
"state": "CA",
"latitude": 34.0128,
"longitude": -118.2841,
"capacity": 22000,
"sport": "MLS",
"primary_team_abbrevs": [
"LAFC"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_chase_stadium",
"name": "Chase Stadium",
"city": "Fort Lauderdale",
"state": "FL",
"latitude": 26.1902,
"longitude": -80.163,
"capacity": 21550,
"sport": "MLS",
"primary_team_abbrevs": [
"MIA"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_allianz_field",
"name": "Allianz Field",
"city": "St. Paul",
"state": "MN",
"latitude": 44.9532,
"longitude": -93.1653,
"capacity": 19400,
"sport": "MLS",
"primary_team_abbrevs": [
"MIN"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_stade_saputo",
"name": "Stade Saputo",
"city": "Montreal",
"state": "QC",
"latitude": 45.5628,
"longitude": -73.553,
"capacity": 19619,
"sport": "MLS",
"primary_team_abbrevs": [
"MTL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_geodis_park",
"name": "Geodis Park",
"city": "Nashville",
"state": "TN",
"latitude": 36.1303,
"longitude": -86.7663,
"capacity": 30000,
"sport": "MLS",
"primary_team_abbrevs": [
"NSH"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_gillette_stadium",
"name": "Gillette Stadium",
"city": "Foxborough",
"state": "MA",
"latitude": 42.0909,
"longitude": -71.2643,
"capacity": 65878,
"sport": "MLS",
"primary_team_abbrevs": [
"NER"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_yankee_stadium",
"name": "Yankee Stadium",
"city": "New York",
"state": "NY",
"latitude": 40.8296,
"longitude": -73.9262,
"capacity": 46537,
"sport": "MLS",
"primary_team_abbrevs": [
"NYC"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_red_bull_arena",
"name": "Red Bull Arena",
"city": "Harrison",
"state": "NJ",
"latitude": 40.7368,
"longitude": -74.1503,
"capacity": 25000,
"sport": "MLS",
"primary_team_abbrevs": [
"RBNY"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_interco_stadium",
"name": "Inter&Co Stadium",
"city": "Orlando",
"state": "FL",
"latitude": 28.5411,
"longitude": -81.3899,
"capacity": 25500,
"sport": "MLS",
"primary_team_abbrevs": [
"ORL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_subaru_park",
"name": "Subaru Park",
"city": "Chester",
"state": "PA",
"latitude": 39.8328,
"longitude": -75.3789,
"capacity": 18500,
"sport": "MLS",
"primary_team_abbrevs": [
"PHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_providence_park",
"name": "Providence Park",
"city": "Portland",
"state": "OR",
"latitude": 45.5217,
"longitude": -122.6917,
"capacity": 25218,
"sport": "MLS",
"primary_team_abbrevs": [
"POR"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_america_first_field",
"name": "America First Field",
"city": "Sandy",
"state": "UT",
"latitude": 40.5828,
"longitude": -111.8933,
"capacity": 20213,
"sport": "MLS",
"primary_team_abbrevs": [
"RSL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_paypal_park",
"name": "PayPal Park",
"city": "San Jose",
"state": "CA",
"latitude": 37.3513,
"longitude": -121.9253,
"capacity": 18000,
"sport": "MLS",
"primary_team_abbrevs": [
"SJE"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_lumen_field",
"name": "Lumen Field",
"city": "Seattle",
"state": "WA",
"latitude": 47.5952,
"longitude": -122.3316,
"capacity": 68740,
"sport": "MLS",
"primary_team_abbrevs": [
"SEA"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_childrens_mercy_park",
"name": "Children's Mercy Park",
"city": "Kansas City",
"state": "KS",
"latitude": 39.1218,
"longitude": -94.8234,
"capacity": 18467,
"sport": "MLS",
"primary_team_abbrevs": [
"SKC"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_citypark",
"name": "CityPark",
"city": "St. Louis",
"state": "MO",
"latitude": 38.6322,
"longitude": -90.2094,
"capacity": 22500,
"sport": "MLS",
"primary_team_abbrevs": [
"STL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bmo_field",
"name": "BMO Field",
"city": "Toronto",
"state": "ON",
"latitude": 43.6332,
"longitude": -79.4186,
"capacity": 30000,
"sport": "MLS",
"primary_team_abbrevs": [
"TOR"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bc_place",
"name": "BC Place",
"city": "Vancouver",
"state": "BC",
"latitude": 49.2768,
"longitude": -123.1118,
"capacity": 54320,
"sport": "MLS",
"primary_team_abbrevs": [
"VAN"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_snapdragon_stadium",
"name": "Snapdragon Stadium",
"city": "San Diego",
"state": "CA",
"latitude": 32.7839,
"longitude": -117.1224,
"capacity": 35000,
"sport": "MLS",
"primary_team_abbrevs": [
"SDG"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_bmo_stadium",
"name": "BMO Stadium",
"city": "Los Angeles",
"state": "CA",
"latitude": 34.0128,
"longitude": -118.2841,
"capacity": 22000,
"sport": "NWSL",
"primary_team_abbrevs": [
"ANG"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_paypal_park",
"name": "PayPal Park",
"city": "San Jose",
"state": "CA",
"latitude": 37.3513,
"longitude": -121.9253,
"capacity": 18000,
"sport": "NWSL",
"primary_team_abbrevs": [
"BAY"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_seatgeek_stadium",
"name": "SeatGeek Stadium",
"city": "Chicago",
"state": "IL",
"latitude": 41.6462,
"longitude": -87.7304,
"capacity": 20000,
"sport": "NWSL",
"primary_team_abbrevs": [
"CHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_shell_energy_stadium",
"name": "Shell Energy Stadium",
"city": "Houston",
"state": "TX",
"latitude": 29.7523,
"longitude": -95.3522,
"capacity": 22039,
"sport": "NWSL",
"primary_team_abbrevs": [
"HOU"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_cpkc_stadium",
"name": "CPKC Stadium",
"city": "Kansas City",
"state": "KS",
"latitude": 39.0851,
"longitude": -94.5582,
"capacity": 11500,
"sport": "NWSL",
"primary_team_abbrevs": [
"KCC"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_red_bull_arena",
"name": "Red Bull Arena",
"city": "Harrison",
"state": "NJ",
"latitude": 40.7368,
"longitude": -74.1503,
"capacity": 25000,
"sport": "NWSL",
"primary_team_abbrevs": [
"NJY"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_wakemed_soccer_park",
"name": "WakeMed Soccer Park",
"city": "Cary",
"state": "NC",
"latitude": 35.8589,
"longitude": -78.7989,
"capacity": 10000,
"sport": "NWSL",
"primary_team_abbrevs": [
"NCC"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_interco_stadium",
"name": "Inter&Co Stadium",
"city": "Orlando",
"state": "FL",
"latitude": 28.5411,
"longitude": -81.3899,
"capacity": 25500,
"sport": "NWSL",
"primary_team_abbrevs": [
"ORL"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_providence_park",
"name": "Providence Park",
"city": "Portland",
"state": "OR",
"latitude": 45.5217,
"longitude": -122.6917,
"capacity": 25218,
"sport": "NWSL",
"primary_team_abbrevs": [
"POR"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_lumen_field",
"name": "Lumen Field",
"city": "Seattle",
"state": "WA",
"latitude": 47.5952,
"longitude": -122.3316,
"capacity": 68740,
"sport": "NWSL",
"primary_team_abbrevs": [
"RGN"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_snapdragon_stadium",
"name": "Snapdragon Stadium",
"city": "San Diego",
"state": "CA",
"latitude": 32.7839,
"longitude": -117.1224,
"capacity": 35000,
"sport": "NWSL",
"primary_team_abbrevs": [
"SDW"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_america_first_field",
"name": "America First Field",
"city": "Sandy",
"state": "UT",
"latitude": 40.5828,
"longitude": -111.8933,
"capacity": 20213,
"sport": "NWSL",
"primary_team_abbrevs": [
"UTA"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_audi_field",
"name": "Audi Field",
"city": "Washington",
"state": "DC",
"latitude": 38.8686,
"longitude": -77.0128,
"capacity": 20000,
"sport": "NWSL",
"primary_team_abbrevs": [
"WSH"
],
"year_opened": null
}
]

View File

@@ -31,9 +31,24 @@ from dataclasses import dataclass, asdict
# Import pipeline components
from scrape_schedules import (
scrape_nba_basketball_reference,
scrape_mlb_statsapi,
scrape_nhl_hockey_reference,
ScraperSource, scrape_with_fallback,
# NBA sources
scrape_nba_basketball_reference, scrape_nba_espn, scrape_nba_cbssports,
# MLB sources
scrape_mlb_statsapi, scrape_mlb_baseball_reference, scrape_mlb_espn,
# NHL sources
scrape_nhl_hockey_reference, scrape_nhl_espn, scrape_nhl_api,
# NFL sources
scrape_nfl_espn, scrape_nfl_pro_football_reference, scrape_nfl_cbssports,
# WNBA sources
scrape_wnba_espn, scrape_wnba_basketball_reference, scrape_wnba_cbssports,
# MLS sources
scrape_mls_espn, scrape_mls_fbref, scrape_mls_mlssoccer,
# NWSL sources
scrape_nwsl_espn, scrape_nwsl_fbref, scrape_nwsl_nwslsoccer,
# CBB sources
scrape_cbb_espn, scrape_cbb_sports_reference, scrape_cbb_cbssports,
# Utilities
generate_stadiums_from_teams,
assign_stable_ids,
export_to_json,
@@ -114,28 +129,90 @@ def run_pipeline(
all_stadiums = generate_stadiums_from_teams()
print(f" Generated {len(all_stadiums)} stadiums from team data")
# Scrape NBA
# Scrape all sports with multi-source fallback
print_section(f"NBA {season}")
nba_games = scrape_nba_basketball_reference(season)
nba_sources = [
ScraperSource('Basketball-Reference', scrape_nba_basketball_reference, priority=1, min_games=500),
ScraperSource('ESPN', scrape_nba_espn, priority=2, min_games=500),
ScraperSource('CBS Sports', scrape_nba_cbssports, priority=3, min_games=100),
]
nba_games = scrape_with_fallback('NBA', season, nba_sources)
nba_season = f"{season-1}-{str(season)[2:]}"
nba_games = assign_stable_ids(nba_games, 'NBA', nba_season)
all_games.extend(nba_games)
print(f" Scraped {len(nba_games)} NBA games")
# Scrape MLB
print_section(f"MLB {season}")
mlb_games = scrape_mlb_statsapi(season)
mlb_sources = [
ScraperSource('MLB Stats API', scrape_mlb_statsapi, priority=1, min_games=1000),
ScraperSource('Baseball-Reference', scrape_mlb_baseball_reference, priority=2, min_games=500),
ScraperSource('ESPN', scrape_mlb_espn, priority=3, min_games=500),
]
mlb_games = scrape_with_fallback('MLB', season, mlb_sources)
mlb_games = assign_stable_ids(mlb_games, 'MLB', str(season))
all_games.extend(mlb_games)
print(f" Scraped {len(mlb_games)} MLB games")
# Scrape NHL
print_section(f"NHL {season}")
nhl_games = scrape_nhl_hockey_reference(season)
nhl_sources = [
ScraperSource('Hockey-Reference', scrape_nhl_hockey_reference, priority=1, min_games=500),
ScraperSource('ESPN', scrape_nhl_espn, priority=2, min_games=500),
ScraperSource('NHL API', scrape_nhl_api, priority=3, min_games=100),
]
nhl_games = scrape_with_fallback('NHL', season, nhl_sources)
nhl_season = f"{season-1}-{str(season)[2:]}"
nhl_games = assign_stable_ids(nhl_games, 'NHL', nhl_season)
all_games.extend(nhl_games)
print(f" Scraped {len(nhl_games)} NHL games")
print_section(f"NFL {season}")
nfl_sources = [
ScraperSource('ESPN', scrape_nfl_espn, priority=1, min_games=200),
ScraperSource('Pro-Football-Reference', scrape_nfl_pro_football_reference, priority=2, min_games=200),
ScraperSource('CBS Sports', scrape_nfl_cbssports, priority=3, min_games=100),
]
nfl_games = scrape_with_fallback('NFL', season, nfl_sources)
nfl_season = f"{season-1}-{str(season)[2:]}"
nfl_games = assign_stable_ids(nfl_games, 'NFL', nfl_season)
all_games.extend(nfl_games)
print_section(f"WNBA {season}")
wnba_sources = [
ScraperSource('ESPN', scrape_wnba_espn, priority=1, min_games=100),
ScraperSource('Basketball-Reference', scrape_wnba_basketball_reference, priority=2, min_games=100),
ScraperSource('CBS Sports', scrape_wnba_cbssports, priority=3, min_games=50),
]
wnba_games = scrape_with_fallback('WNBA', season, wnba_sources)
wnba_games = assign_stable_ids(wnba_games, 'WNBA', str(season))
all_games.extend(wnba_games)
print_section(f"MLS {season}")
mls_sources = [
ScraperSource('ESPN', scrape_mls_espn, priority=1, min_games=200),
ScraperSource('FBref', scrape_mls_fbref, priority=2, min_games=100),
ScraperSource('MLSSoccer.com', scrape_mls_mlssoccer, priority=3, min_games=100),
]
mls_games = scrape_with_fallback('MLS', season, mls_sources)
mls_games = assign_stable_ids(mls_games, 'MLS', str(season))
all_games.extend(mls_games)
print_section(f"NWSL {season}")
nwsl_sources = [
ScraperSource('ESPN', scrape_nwsl_espn, priority=1, min_games=100),
ScraperSource('FBref', scrape_nwsl_fbref, priority=2, min_games=50),
ScraperSource('NWSL.com', scrape_nwsl_nwslsoccer, priority=3, min_games=50),
]
nwsl_games = scrape_with_fallback('NWSL', season, nwsl_sources)
nwsl_games = assign_stable_ids(nwsl_games, 'NWSL', str(season))
all_games.extend(nwsl_games)
print_section(f"CBB {season}")
cbb_sources = [
ScraperSource('ESPN', scrape_cbb_espn, priority=1, min_games=1000),
ScraperSource('Sports-Reference', scrape_cbb_sports_reference, priority=2, min_games=500),
ScraperSource('CBS Sports', scrape_cbb_cbssports, priority=3, min_games=300),
]
cbb_games = scrape_with_fallback('CBB', season, cbb_sources)
cbb_season = f"{season-1}-{str(season)[2:]}"
cbb_games = assign_stable_ids(cbb_games, 'CBB', cbb_season)
all_games.extend(cbb_games)
# Export raw data
print_section("Exporting Raw Data")
@@ -148,16 +225,36 @@ def run_pipeline(
else:
print_header("LOADING EXISTING RAW DATA")
games_file = output_dir / 'games.json'
stadiums_file = output_dir / 'stadiums.json'
# Try loading from new structure first (games/*.json)
games_dir = output_dir / 'games'
raw_games = []
with open(games_file) as f:
raw_games = json.load(f)
print(f" Loaded {len(raw_games)} raw games")
if games_dir.exists() and any(games_dir.glob('*.json')):
print_section("Loading from games/ directory")
for games_file in sorted(games_dir.glob('*.json')):
with open(games_file) as f:
file_games = json.load(f)
raw_games.extend(file_games)
print(f" Loaded {len(file_games):,} games from {games_file.name}")
else:
# Fallback to legacy games.json
print_section("Loading from legacy games.json")
games_file = output_dir / 'games.json'
with open(games_file) as f:
raw_games = json.load(f)
with open(stadiums_file) as f:
raw_stadiums = json.load(f)
print(f" Loaded {len(raw_stadiums)} raw stadiums")
print(f" Total: {len(raw_games):,} raw games")
# Try loading stadiums from canonical/ first, then legacy
canonical_dir = output_dir / 'canonical'
if (canonical_dir / 'stadiums.json').exists():
with open(canonical_dir / 'stadiums.json') as f:
raw_stadiums = json.load(f)
print(f" Loaded {len(raw_stadiums)} raw stadiums from canonical/stadiums.json")
else:
with open(output_dir / 'stadiums.json') as f:
raw_stadiums = json.load(f)
print(f" Loaded {len(raw_stadiums)} raw stadiums from stadiums.json")
# =========================================================================
# STAGE 2: CANONICALIZE STADIUMS
@@ -242,13 +339,32 @@ def run_pipeline(
for issue, count in by_issue.items():
print(f" - {issue}: {count}")
# Export
games_canonical_path = output_dir / 'games_canonical.json'
# Export games to new structure: canonical/games/{sport}_{season}.json
canonical_games_dir = output_dir / 'canonical' / 'games'
canonical_games_dir.mkdir(parents=True, exist_ok=True)
# Group games by sport and season
games_by_sport_season = {}
for game in canonical_games_list:
sport = game.sport.lower()
season = game.season
key = f"{sport}_{season}"
if key not in games_by_sport_season:
games_by_sport_season[key] = []
games_by_sport_season[key].append(game)
# Export each sport/season file
for key, sport_games in sorted(games_by_sport_season.items()):
filepath = canonical_games_dir / f"{key}.json"
with open(filepath, 'w') as f:
json.dump([asdict(g) for g in sport_games], f, indent=2)
print(f" Exported {len(sport_games):,} games to canonical/games/{key}.json")
# Also export combined games_canonical.json for backward compatibility
games_canonical_path = output_dir / 'games_canonical.json'
with open(games_canonical_path, 'w') as f:
json.dump([asdict(g) for g in canonical_games_list], f, indent=2)
print(f" Exported to {games_canonical_path}")
print(f" Exported combined to {games_canonical_path}")
# =========================================================================
# STAGE 5: VALIDATE
@@ -320,7 +436,8 @@ def run_pipeline(
print(f" - {output_dir / 'stadiums_canonical.json'}")
print(f" - {output_dir / 'stadium_aliases.json'}")
print(f" - {output_dir / 'teams_canonical.json'}")
print(f" - {output_dir / 'games_canonical.json'}")
print(f" - {output_dir / 'games_canonical.json'} (combined)")
print(f" - {output_dir / 'canonical' / 'games' / '*.json'} (by sport/season)")
print(f" - {output_dir / 'canonicalization_validation.json'}")
print()

View File

@@ -23,10 +23,24 @@ from enum import Enum
# Import our modules
from scrape_schedules import (
Game, Stadium,
scrape_nba_basketball_reference,
scrape_mlb_statsapi, scrape_mlb_baseball_reference,
scrape_nhl_hockey_reference,
Game, Stadium, ScraperSource, scrape_with_fallback,
# NBA sources
scrape_nba_basketball_reference, scrape_nba_espn, scrape_nba_cbssports,
# MLB sources
scrape_mlb_statsapi, scrape_mlb_baseball_reference, scrape_mlb_espn,
# NHL sources
scrape_nhl_hockey_reference, scrape_nhl_espn, scrape_nhl_api,
# NFL sources
scrape_nfl_espn, scrape_nfl_pro_football_reference, scrape_nfl_cbssports,
# WNBA sources
scrape_wnba_espn, scrape_wnba_basketball_reference, scrape_wnba_cbssports,
# MLS sources
scrape_mls_espn, scrape_mls_fbref, scrape_mls_mlssoccer,
# NWSL sources
scrape_nwsl_espn, scrape_nwsl_fbref, scrape_nwsl_nwslsoccer,
# CBB sources
scrape_cbb_espn, scrape_cbb_sports_reference, scrape_cbb_cbssports,
# Utilities
generate_stadiums_from_teams,
export_to_json,
assign_stable_ids,
@@ -119,10 +133,15 @@ def run_pipeline(
all_stadiums = generate_stadiums_from_teams()
print(f" Generated {len(all_stadiums)} stadiums from team data")
# Scrape by sport
# Scrape by sport with multi-source fallback
if sport in ['nba', 'all']:
print_section(f"NBA {season}")
nba_games = scrape_nba_basketball_reference(season)
nba_sources = [
ScraperSource('Basketball-Reference', scrape_nba_basketball_reference, priority=1, min_games=500),
ScraperSource('ESPN', scrape_nba_espn, priority=2, min_games=500),
ScraperSource('CBS Sports', scrape_nba_cbssports, priority=3, min_games=100),
]
nba_games = scrape_with_fallback('NBA', season, nba_sources)
nba_season = f"{season-1}-{str(season)[2:]}"
nba_games = assign_stable_ids(nba_games, 'NBA', nba_season)
all_games.extend(nba_games)
@@ -130,19 +149,91 @@ def run_pipeline(
if sport in ['mlb', 'all']:
print_section(f"MLB {season}")
mlb_games = scrape_mlb_statsapi(season)
# MLB API uses official gamePk - already stable
mlb_sources = [
ScraperSource('MLB Stats API', scrape_mlb_statsapi, priority=1, min_games=1000),
ScraperSource('Baseball-Reference', scrape_mlb_baseball_reference, priority=2, min_games=500),
ScraperSource('ESPN', scrape_mlb_espn, priority=3, min_games=500),
]
mlb_games = scrape_with_fallback('MLB', season, mlb_sources)
mlb_games = assign_stable_ids(mlb_games, 'MLB', str(season))
all_games.extend(mlb_games)
games_by_sport['MLB'] = len(mlb_games)
if sport in ['nhl', 'all']:
print_section(f"NHL {season}")
nhl_games = scrape_nhl_hockey_reference(season)
nhl_sources = [
ScraperSource('Hockey-Reference', scrape_nhl_hockey_reference, priority=1, min_games=500),
ScraperSource('ESPN', scrape_nhl_espn, priority=2, min_games=500),
ScraperSource('NHL API', scrape_nhl_api, priority=3, min_games=100),
]
nhl_games = scrape_with_fallback('NHL', season, nhl_sources)
nhl_season = f"{season-1}-{str(season)[2:]}"
nhl_games = assign_stable_ids(nhl_games, 'NHL', nhl_season)
all_games.extend(nhl_games)
games_by_sport['NHL'] = len(nhl_games)
if sport in ['nfl', 'all']:
print_section(f"NFL {season}")
nfl_sources = [
ScraperSource('ESPN', scrape_nfl_espn, priority=1, min_games=200),
ScraperSource('Pro-Football-Reference', scrape_nfl_pro_football_reference, priority=2, min_games=200),
ScraperSource('CBS Sports', scrape_nfl_cbssports, priority=3, min_games=100),
]
nfl_games = scrape_with_fallback('NFL', season, nfl_sources)
nfl_season = f"{season-1}-{str(season)[2:]}"
nfl_games = assign_stable_ids(nfl_games, 'NFL', nfl_season)
all_games.extend(nfl_games)
games_by_sport['NFL'] = len(nfl_games)
if sport in ['wnba', 'all']:
print_section(f"WNBA {season}")
wnba_sources = [
ScraperSource('ESPN', scrape_wnba_espn, priority=1, min_games=100),
ScraperSource('Basketball-Reference', scrape_wnba_basketball_reference, priority=2, min_games=100),
ScraperSource('CBS Sports', scrape_wnba_cbssports, priority=3, min_games=50),
]
wnba_games = scrape_with_fallback('WNBA', season, wnba_sources)
wnba_games = assign_stable_ids(wnba_games, 'WNBA', str(season))
all_games.extend(wnba_games)
games_by_sport['WNBA'] = len(wnba_games)
if sport in ['mls', 'all']:
print_section(f"MLS {season}")
mls_sources = [
ScraperSource('ESPN', scrape_mls_espn, priority=1, min_games=200),
ScraperSource('FBref', scrape_mls_fbref, priority=2, min_games=100),
ScraperSource('MLSSoccer.com', scrape_mls_mlssoccer, priority=3, min_games=100),
]
mls_games = scrape_with_fallback('MLS', season, mls_sources)
mls_games = assign_stable_ids(mls_games, 'MLS', str(season))
all_games.extend(mls_games)
games_by_sport['MLS'] = len(mls_games)
if sport in ['nwsl', 'all']:
print_section(f"NWSL {season}")
nwsl_sources = [
ScraperSource('ESPN', scrape_nwsl_espn, priority=1, min_games=100),
ScraperSource('FBref', scrape_nwsl_fbref, priority=2, min_games=50),
ScraperSource('NWSL.com', scrape_nwsl_nwslsoccer, priority=3, min_games=50),
]
nwsl_games = scrape_with_fallback('NWSL', season, nwsl_sources)
nwsl_games = assign_stable_ids(nwsl_games, 'NWSL', str(season))
all_games.extend(nwsl_games)
games_by_sport['NWSL'] = len(nwsl_games)
if sport in ['cbb', 'all']:
print_section(f"CBB {season}")
cbb_sources = [
ScraperSource('ESPN', scrape_cbb_espn, priority=1, min_games=1000),
ScraperSource('Sports-Reference', scrape_cbb_sports_reference, priority=2, min_games=500),
ScraperSource('CBS Sports', scrape_cbb_cbssports, priority=3, min_games=300),
]
cbb_games = scrape_with_fallback('CBB', season, cbb_sources)
cbb_season = f"{season-1}-{str(season)[2:]}"
cbb_games = assign_stable_ids(cbb_games, 'CBB', cbb_season)
all_games.extend(cbb_games)
games_by_sport['CBB'] = len(cbb_games)
# Export data
print_section("Exporting Data")
export_to_json(all_games, all_stadiums, output_dir)
@@ -233,6 +324,17 @@ def run_pipeline(
if count < 75 or count > 90:
print(f" NHL: {team} has {count} games (expected ~82)")
if sport in ['nfl', 'all']:
nfl_games = [g for g in all_games if g.sport == 'NFL']
team_counts = {}
for g in nfl_games:
team_counts[g.home_team_abbrev] = team_counts.get(g.home_team_abbrev, 0) + 1
team_counts[g.away_team_abbrev] = team_counts.get(g.away_team_abbrev, 0) + 1
for team, count in sorted(team_counts.items()):
if count < 15 or count > 20:
print(f" NFL: {team} has {count} games (expected ~17)")
# =========================================================================
# PHASE 3: GENERATE REPORT
# =========================================================================
@@ -396,7 +498,7 @@ Examples:
help='Season year (default: 2025)'
)
parser.add_argument(
'--sport', choices=['nba', 'mlb', 'nhl', 'all'], default='all',
'--sport', choices=['nba', 'mlb', 'nhl', 'nfl', 'wnba', 'mls', 'nwsl', 'cbb', 'all'], default='all',
help='Sport to process (default: all)'
)
parser.add_argument(

File diff suppressed because it is too large Load Diff

1092
Scripts/sportstime.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,32 @@ EXPECTED_GAMES = {
'max': 168,
'description': 'MLB regular season (162 games)'
},
'nfl': {
'expected': 17,
'min': 15,
'max': 20,
'description': 'NFL regular season (17 games)'
},
'wnba': {
'expected': 40,
'min': 35,
'max': 45,
'description': 'WNBA regular season (40 games)'
},
'mls': {
'expected': 34,
'min': 30,
'max': 40,
'description': 'MLS regular season (34 games)'
},
'nwsl': {
'expected': 26,
'min': 22,
'max': 30,
'description': 'NWSL regular season (26 games)'
},
# Note: CBB doesn't have fixed game counts per "team"
# CBB teams vary widely (30+ games)
}

View File

@@ -22,7 +22,10 @@ from scrape_schedules import (
scrape_nba_basketball_reference,
scrape_mlb_statsapi, scrape_mlb_baseball_reference,
scrape_nhl_hockey_reference,
NBA_TEAMS, MLB_TEAMS, NHL_TEAMS,
scrape_wnba_espn, scrape_mls_espn, scrape_nwsl_espn,
scrape_nfl_espn, scrape_cbb_espn,
NBA_TEAMS, MLB_TEAMS, NHL_TEAMS, WNBA_TEAMS, MLS_TEAMS, NWSL_TEAMS,
NFL_TEAMS,
assign_stable_ids,
)
@@ -136,7 +139,11 @@ def generate_game_key(game: Game) -> str:
def normalize_team_name(name: str, sport: str) -> str:
"""Normalize team name variations."""
teams = {'NBA': NBA_TEAMS, 'MLB': MLB_TEAMS, 'NHL': NHL_TEAMS}.get(sport, {})
teams = {
'NBA': NBA_TEAMS, 'MLB': MLB_TEAMS, 'NHL': NHL_TEAMS,
'WNBA': WNBA_TEAMS, 'MLS': MLS_TEAMS, 'NWSL': NWSL_TEAMS,
'NFL': NFL_TEAMS,
}.get(sport, {})
name_lower = name.lower().strip()
@@ -465,7 +472,7 @@ def main():
parser.add_argument('--data-dir', type=str, default='./data', help='Data directory')
parser.add_argument('--scrape-and-validate', action='store_true', help='Scrape fresh and validate')
parser.add_argument('--season', type=int, default=2025, help='Season year')
parser.add_argument('--sport', choices=['nba', 'mlb', 'nhl', 'all'], default='all')
parser.add_argument('--sport', choices=['nba', 'mlb', 'nhl', 'nfl', 'wnba', 'mls', 'nwsl', 'cbb', 'all'], default='all')
parser.add_argument('--output', type=str, default='./data/validation_report.json')
args = parser.parse_args()

View File

@@ -12,6 +12,9 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
case nhl = "NHL"
case nfl = "NFL"
case mls = "MLS"
case wnba = "WNBA"
case nwsl = "NWSL"
case cbb = "CBB"
var id: String { rawValue }
@@ -22,6 +25,9 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
case .nhl: return "National Hockey League"
case .nfl: return "National Football League"
case .mls: return "Major League Soccer"
case .wnba: return "Women's National Basketball Association"
case .nwsl: return "National Women's Soccer League"
case .cbb: return "College Basketball"
}
}
@@ -32,6 +38,9 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
case .nhl: return "hockey.puck.fill"
case .nfl: return "football.fill"
case .mls: return "soccerball"
case .wnba: return "basketball.fill"
case .nwsl: return "soccerball"
case .cbb: return "basketball.fill"
}
}
@@ -42,6 +51,9 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
case .nhl: return .blue
case .nfl: return .brown
case .mls: return .green
case .wnba: return .purple
case .nwsl: return .teal
case .cbb: return .mint
}
}
@@ -53,6 +65,9 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
case .nhl: return (10, 6) // October - June (wraps)
case .nfl: return (9, 2) // September - February (wraps)
case .mls: return (2, 12) // February - December
case .wnba: return (5, 10) // May - October
case .nwsl: return (3, 11) // March - November
case .cbb: return (11, 4) // November - April (wraps, March Madness)
}
}
@@ -70,8 +85,19 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
}
}
/// Currently supported sports for MVP
/// Currently supported sports
static var supported: [Sport] {
[.mlb, .nba, .nhl]
[.mlb, .nba, .nfl, .nhl, .mls, .wnba, .nwsl, .cbb]
}
}
// MARK: - Array Chunking
extension Array {
/// Splits array into chunks of specified size
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}

View File

@@ -25,7 +25,7 @@ enum PlanningMode: String, Codable, CaseIterable, Identifiable {
var description: String {
switch self {
case .dateRange: return "Find games within a date range"
case .dateRange: return "Shows a curated sample of possible routes — use filters to find your ideal trip"
case .gameFirst: return "Build trip around specific games"
case .locations: return "Plan route between locations"
}

View File

@@ -126,6 +126,9 @@ enum Theme {
static let nhlBlue = Color(hex: "003087")
static let nflBrown = Color(hex: "8B5A2B")
static let mlsGreen = Color(hex: "00A651")
static let wnbaPurple = Color(hex: "FF6F20") // WNBA orange
static let nwslTeal = Color(hex: "009688") // NWSL teal
static let cbbMint = Color(hex: "3EB489") // College Basketball mint
// MARK: - Dark Mode Colors

View File

@@ -215,6 +215,9 @@ extension Sport {
case .nhl: return Theme.nhlBlue
case .nfl: return Theme.nflBrown
case .mls: return Theme.mlsGreen
case .wnba: return Theme.wnbaPurple
case .nwsl: return Theme.nwslTeal
case .cbb: return Theme.cbbMint
}
}
}

View File

@@ -169,19 +169,40 @@ struct HomeView: View {
// MARK: - Quick Actions
private var quickActions: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
let sports = Sport.supported
let rows = sports.chunked(into: 4)
return VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Quick Start")
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
.foregroundStyle(Theme.textPrimary(colorScheme))
HStack(spacing: Theme.Spacing.sm) {
ForEach(Sport.supported) { sport in
QuickSportButton(sport: sport) {
selectedSport = sport
showNewTrip = true
VStack(spacing: Theme.Spacing.md) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
HStack(spacing: Theme.Spacing.sm) {
ForEach(row) { sport in
QuickSportButton(sport: sport) {
selectedSport = sport
showNewTrip = true
}
}
// Fill remaining space if row has fewer than 4 items
if row.count < 4 {
ForEach(0..<(4 - row.count), id: \.self) { _ in
Color.clear.frame(maxWidth: .infinity)
}
}
}
}
}
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
}
@@ -339,30 +360,23 @@ struct QuickSportButton: View {
var body: some View {
Button(action: action) {
VStack(spacing: Theme.Spacing.xs) {
VStack(spacing: 6) {
ZStack {
Circle()
.fill(sport.themeColor.opacity(0.15))
.frame(width: 44, height: 44)
.frame(width: 48, height: 48)
Image(systemName: sport.iconName)
.font(.title2)
.font(.system(size: 20))
.foregroundStyle(sport.themeColor)
}
Text(sport.rawValue)
.font(.system(size: Theme.FontSize.micro, weight: .medium))
.font(.system(size: 10, weight: .medium))
.foregroundStyle(Theme.textSecondary(colorScheme))
}
.frame(maxWidth: .infinity)
.padding(.vertical, Theme.Spacing.sm)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.scaleEffect(isPressed ? 0.95 : 1.0)
.scaleEffect(isPressed ? 0.9 : 1.0)
}
.buttonStyle(.plain)
.simultaneousGesture(

View File

@@ -244,13 +244,7 @@ struct SettingsView: View {
// MARK: - Helpers
private func sportColor(for sport: Sport) -> Color {
switch sport {
case .mlb: return .red
case .nba: return .orange
case .nhl: return .blue
case .nfl: return .green
case .mls: return .purple
}
sport.themeColor
}
}

View File

@@ -58,8 +58,24 @@ final class TripCreationViewModel {
}
// Dates
var startDate: Date = Date()
var endDate: Date = Date().addingTimeInterval(86400 * 7)
var startDate: Date = Date() {
didSet {
// Clear cached games when start date changes
if !Calendar.current.isDate(startDate, inSameDayAs: oldValue) {
availableGames = []
games = []
}
}
}
var endDate: Date = Date().addingTimeInterval(86400 * 7) {
didSet {
// Clear cached games when end date changes
if !Calendar.current.isDate(endDate, inSameDayAs: oldValue) {
availableGames = []
games = []
}
}
}
// Trip duration for game-first mode (days before/after selected games)
var tripBufferDays: Int = 2

View File

@@ -524,22 +524,36 @@ struct TripCreationView: View {
}
private var sportsSection: some View {
ThemedSection(title: "Sports") {
HStack(spacing: Theme.Spacing.sm) {
ForEach(Sport.supported) { sport in
SportSelectionChip(
sport: sport,
isSelected: viewModel.selectedSports.contains(sport),
onTap: {
if viewModel.selectedSports.contains(sport) {
viewModel.selectedSports.remove(sport)
} else {
viewModel.selectedSports.insert(sport)
let sports = Sport.supported
let rows = sports.chunked(into: 4)
return ThemedSection(title: "Sports") {
VStack(spacing: Theme.Spacing.sm) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
HStack(spacing: Theme.Spacing.sm) {
ForEach(row) { sport in
SportSelectionChip(
sport: sport,
isSelected: viewModel.selectedSports.contains(sport),
onTap: {
if viewModel.selectedSports.contains(sport) {
viewModel.selectedSports.remove(sport)
} else {
viewModel.selectedSports.insert(sport)
}
}
)
}
// Fill remaining space if row has fewer than 4 items
if row.count < 4 {
ForEach(0..<(4 - row.count), id: \.self) { _ in
Color.clear.frame(maxWidth: .infinity)
}
}
)
}
}
}
.padding(.vertical, Theme.Spacing.xs)
}
}
@@ -2139,27 +2153,45 @@ struct SportSelectionChip: View {
let isSelected: Bool
let onTap: () -> Void
@Environment(\.colorScheme) private var colorScheme
@State private var isPressed = false
var body: some View {
Button(action: onTap) {
VStack(spacing: Theme.Spacing.xs) {
VStack(spacing: 6) {
ZStack {
Circle()
.fill(isSelected ? sport.themeColor : sport.themeColor.opacity(0.15))
.frame(width: 44, height: 44)
.frame(width: 48, height: 48)
.overlay {
if isSelected {
Circle()
.stroke(sport.themeColor.opacity(0.3), lineWidth: 3)
.frame(width: 54, height: 54)
}
}
Image(systemName: sport.iconName)
.font(.title3)
.font(.system(size: 20))
.foregroundStyle(isSelected ? .white : sport.themeColor)
}
Text(sport.rawValue)
.font(.system(size: Theme.FontSize.micro, weight: .medium))
.font(.system(size: 10, weight: isSelected ? .semibold : .medium))
.foregroundStyle(isSelected ? Theme.textPrimary(colorScheme) : Theme.textSecondary(colorScheme))
}
.frame(maxWidth: .infinity)
.scaleEffect(isPressed ? 0.9 : 1.0)
}
.buttonStyle(.plain)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
withAnimation(Theme.Animation.spring) { isPressed = true }
}
.onEnded { _ in
withAnimation(Theme.Animation.spring) { isPressed = false }
}
)
}
}

View File

@@ -2,7 +2,7 @@
// GameDAGRouter.swift
// SportsTime
//
// Time-expanded DAG + Beam Search algorithm for route finding.
// DAG-based route finding with multi-dimensional diversity.
//
// Key insight: This is NOT "which subset of N games should I attend?"
// This IS: "what time-respecting paths exist through a graph of games?"
@@ -10,11 +10,14 @@
// The algorithm:
// 1. Bucket games by calendar day
// 2. Build directed edges where time moves forward AND driving is feasible
// 3. Beam search: keep top K paths at each depth
// 4. Dominance pruning: discard inferior paths
// 3. Generate routes via beam search
// 4. Diversity pruning: ensure routes span full range of games, cities, miles, and days
//
// Complexity: O(days × beamWidth × avgNeighbors) 900 operations for 5-day, 78-game scenario
// (vs 2^78 for naive subset enumeration)
// The diversity system ensures users see:
// - Short trips (2-3 cities) AND long trips (5+ cities)
// - Quick trips (2-3 games) AND packed trips (5+ games)
// - Low mileage AND high mileage options
// - Short duration AND long duration trips
//
import Foundation
@@ -24,12 +27,11 @@ enum GameDAGRouter {
// MARK: - Configuration
/// Default beam width - how many partial routes to keep at each step
/// Increased to ensure we preserve diverse route lengths (short and long trips)
private static let defaultBeamWidth = 50
/// Default beam width during expansion
private static let defaultBeamWidth = 100
/// Maximum options to return (increased to provide more diverse trip lengths)
private static let maxOptions = 50
/// Maximum options to return (diverse sample)
private static let maxOptions = 75
/// Buffer time after game ends before we can depart (hours)
private static let gameEndBufferHours: Double = 3.0
@@ -37,21 +39,55 @@ enum GameDAGRouter {
/// Maximum days ahead to consider for next game (1 = next day only, 5 = allows multi-day drives)
private static let maxDayLookahead = 5
// MARK: - Route Profile
/// Captures the key metrics of a route for diversity analysis
private struct RouteProfile {
let route: [Game]
let gameCount: Int
let cityCount: Int
let totalMiles: Double
let tripDays: Int
// Bucket indices for stratified sampling
var gameBucket: Int { min(gameCount - 1, 5) } // 1, 2, 3, 4, 5, 6+
var cityBucket: Int { min(cityCount - 1, 5) } // 1, 2, 3, 4, 5, 6+
var milesBucket: Int {
switch totalMiles {
case ..<500: return 0 // Short
case 500..<1000: return 1 // Medium
case 1000..<2000: return 2 // Long
case 2000..<3000: return 3 // Very long
default: return 4 // Epic
}
}
var daysBucket: Int { min(tripDays - 1, 6) } // 1-7+ days
/// Composite key for exact deduplication
var uniqueKey: String {
route.map { $0.id.uuidString }.joined(separator: "-")
}
}
// MARK: - Public API
/// Finds best routes through the game graph using DAG + beam search.
/// Finds routes through the game graph with multi-dimensional diversity.
///
/// This replaces the exponential GeographicRouteExplorer with a polynomial-time algorithm.
/// Returns a curated sample that spans the full range of:
/// - Number of games (2-game quickies to 6+ game marathons)
/// - Number of cities (2-city to 6+ city routes)
/// - Total miles (short drives to cross-country epics)
/// - Trip duration (weekend getaways to week-long adventures)
///
/// - Parameters:
/// - games: All games to consider, in any order (will be sorted internally)
/// - games: All games to consider
/// - stadiums: Dictionary mapping stadium IDs to Stadium objects
/// - constraints: Driving constraints (number of drivers, max hours per day)
/// - anchorGameIds: Games that MUST appear in every valid route (for Scenario B)
/// - constraints: Driving constraints (max hours per day)
/// - anchorGameIds: Games that MUST appear in every valid route
/// - allowRepeatCities: If false, each city can only appear once in a route
/// - beamWidth: How many partial routes to keep at each depth (default 30)
/// - beamWidth: How many partial routes to keep during expansion
///
/// - Returns: Array of valid game combinations, sorted by score (most games, least driving)
/// - Returns: Array of diverse route options
///
static func findRoutes(
games: [Game],
@@ -65,21 +101,18 @@ enum GameDAGRouter {
// Edge cases
guard !games.isEmpty else { return [] }
if games.count == 1 {
// Single game - just return it if it satisfies anchors
if anchorGameIds.isEmpty || anchorGameIds.contains(games[0].id) {
return [games]
}
return []
}
if games.count == 2 {
// Two games - check if both are reachable
let sorted = games.sorted { $0.startTime < $1.startTime }
if canTransition(from: sorted[0], to: sorted[1], stadiums: stadiums, constraints: constraints) {
if anchorGameIds.isSubset(of: Set(sorted.map { $0.id })) {
return [sorted]
}
}
// Can't connect them - return individual games if they satisfy anchors
if anchorGameIds.isEmpty {
return [[sorted[0]], [sorted[1]]]
}
@@ -95,18 +128,9 @@ enum GameDAGRouter {
guard !sortedDays.isEmpty else { return [] }
// Step 3: Initialize beam with first day's games
// Step 3: Initialize beam with first few days' games as starting points
var beam: [[Game]] = []
if let firstDayGames = buckets[sortedDays[0]] {
for game in firstDayGames {
beam.append([game])
}
}
// Also include option to skip first day entirely and start later
// (handled by having multiple starting points in beam)
for dayIndex in sortedDays.dropFirst().prefix(maxDayLookahead - 1) {
for dayIndex in sortedDays.prefix(maxDayLookahead) {
if let dayGames = buckets[dayIndex] {
for game in dayGames {
beam.append([game])
@@ -114,9 +138,8 @@ enum GameDAGRouter {
}
}
// Step 4: Expand beam day by day
for (_, dayIndex) in sortedDays.dropFirst().enumerated() {
for dayIndex in sortedDays.dropFirst() {
let todaysGames = buckets[dayIndex] ?? []
var nextBeam: [[Game]] = []
@@ -124,36 +147,34 @@ enum GameDAGRouter {
guard let lastGame = path.last else { continue }
let lastGameDay = dayIndexFor(lastGame.startTime, referenceDate: sortedGames[0].startTime)
// Only consider games on this day or within lookahead
// Skip if this day is too far ahead for this route
if dayIndex > lastGameDay + maxDayLookahead {
// This path is too far behind, keep it as-is
nextBeam.append(path)
continue
}
// Try adding each of today's games
for candidate in todaysGames {
// Check for repeat city violation during route building
// Check for repeat city violation
if !allowRepeatCities {
let candidateCity = stadiums[candidate.stadiumId]?.city ?? ""
let pathCities = Set(path.compactMap { stadiums[$0.stadiumId]?.city })
if pathCities.contains(candidateCity) {
continue // Skip - would violate allowRepeatCities
continue
}
}
if canTransition(from: lastGame, to: candidate, stadiums: stadiums, constraints: constraints) {
let newPath = path + [candidate]
nextBeam.append(newPath)
nextBeam.append(path + [candidate])
}
}
// Also keep the path without adding a game today (allows off-days)
// Keep the path without adding a game today
nextBeam.append(path)
}
// Dominance pruning + beam truncation
beam = pruneAndTruncate(nextBeam, beamWidth: beamWidth, stadiums: stadiums)
// Diversity-aware pruning during expansion
beam = diversityPrune(nextBeam, stadiums: stadiums, targetCount: beamWidth)
}
// Step 5: Filter routes that contain all anchors
@@ -162,21 +183,15 @@ enum GameDAGRouter {
return anchorGameIds.isSubset(of: pathGameIds)
}
// Step 6: Ensure geographic diversity in results
// Group routes by their primary region (city with most games)
// Then pick the best route from each region
// Step 6: Final diversity selection
let finalRoutes = selectDiverseRoutes(routesWithAnchors, stadiums: stadiums, maxCount: maxOptions)
print("🔍 DAG: Input games=\(games.count), beam final=\(beam.count), withAnchors=\(routesWithAnchors.count), final=\(finalRoutes.count)")
if let best = finalRoutes.first {
print("🔍 DAG: Best route has \(best.count) games")
}
print("🔍 DAG: Input games=\(games.count), beam=\(beam.count), withAnchors=\(routesWithAnchors.count), final=\(finalRoutes.count)")
return finalRoutes
}
/// Compatibility wrapper that matches GeographicRouteExplorer's interface.
/// This allows drop-in replacement in ScenarioAPlanner and ScenarioBPlanner.
static func findAllSensibleRoutes(
from games: [Game],
stadiums: [UUID: Stadium],
@@ -184,9 +199,7 @@ enum GameDAGRouter {
allowRepeatCities: Bool = true,
stopBuilder: ([Game], [UUID: Stadium]) -> [ItineraryStop]
) -> [[Game]] {
// Use default driving constraints
let constraints = DrivingConstraints.default
return findRoutes(
games: games,
stadiums: stadiums,
@@ -196,9 +209,236 @@ enum GameDAGRouter {
)
}
// MARK: - Multi-Dimensional Diversity Selection
/// Selects routes that maximize diversity across all dimensions.
/// Uses stratified sampling to ensure representation of:
/// - Short trips (2-3 games) AND long trips (5+ games)
/// - Few cities (2-3) AND many cities (5+)
/// - Low mileage AND high mileage
/// - Short duration AND long duration
private static func selectDiverseRoutes(
_ routes: [[Game]],
stadiums: [UUID: Stadium],
maxCount: Int
) -> [[Game]] {
guard !routes.isEmpty else { return [] }
// Build profiles for all routes
let profiles = routes.map { route in
buildProfile(for: route, stadiums: stadiums)
}
// Remove duplicates
var uniqueProfiles: [RouteProfile] = []
var seenKeys = Set<String>()
for profile in profiles {
if !seenKeys.contains(profile.uniqueKey) {
seenKeys.insert(profile.uniqueKey)
uniqueProfiles.append(profile)
}
}
// Stratified selection: ensure representation across all buckets
var selected: [RouteProfile] = []
var selectedKeys = Set<String>()
// Pass 1: Ensure at least one route per game count bucket (2, 3, 4, 5, 6+)
let byGames = Dictionary(grouping: uniqueProfiles) { $0.gameBucket }
for bucket in byGames.keys.sorted() {
if selected.count >= maxCount { break }
if let candidates = byGames[bucket]?.sorted(by: { $0.totalMiles < $1.totalMiles }) {
if let best = candidates.first, !selectedKeys.contains(best.uniqueKey) {
selected.append(best)
selectedKeys.insert(best.uniqueKey)
}
}
}
// Pass 2: Ensure at least one route per city count bucket (2, 3, 4, 5, 6+)
let byCities = Dictionary(grouping: uniqueProfiles) { $0.cityBucket }
for bucket in byCities.keys.sorted() {
if selected.count >= maxCount { break }
if let candidates = byCities[bucket]?.filter({ !selectedKeys.contains($0.uniqueKey) }) {
if let best = candidates.sorted(by: { $0.totalMiles < $1.totalMiles }).first {
selected.append(best)
selectedKeys.insert(best.uniqueKey)
}
}
}
// Pass 3: Ensure at least one route per mileage bucket
let byMiles = Dictionary(grouping: uniqueProfiles) { $0.milesBucket }
for bucket in byMiles.keys.sorted() {
if selected.count >= maxCount { break }
if let candidates = byMiles[bucket]?.filter({ !selectedKeys.contains($0.uniqueKey) }) {
if let best = candidates.sorted(by: { $0.gameCount > $1.gameCount }).first {
selected.append(best)
selectedKeys.insert(best.uniqueKey)
}
}
}
// Pass 4: Ensure at least one route per duration bucket
let byDays = Dictionary(grouping: uniqueProfiles) { $0.daysBucket }
for bucket in byDays.keys.sorted() {
if selected.count >= maxCount { break }
if let candidates = byDays[bucket]?.filter({ !selectedKeys.contains($0.uniqueKey) }) {
if let best = candidates.sorted(by: { $0.gameCount > $1.gameCount }).first {
selected.append(best)
selectedKeys.insert(best.uniqueKey)
}
}
}
// Pass 5: Fill remaining slots with diverse combinations
// Create composite buckets for more granular diversity
let remaining = uniqueProfiles.filter { !selectedKeys.contains($0.uniqueKey) }
let byComposite = Dictionary(grouping: remaining) { profile in
"\(profile.gameBucket)-\(profile.cityBucket)-\(profile.milesBucket)"
}
// Round-robin from composite buckets
var compositeKeys = Array(byComposite.keys).sorted()
var indices: [String: Int] = [:]
while selected.count < maxCount && !compositeKeys.isEmpty {
var addedAny = false
for key in compositeKeys {
if selected.count >= maxCount { break }
let idx = indices[key] ?? 0
if let candidates = byComposite[key], idx < candidates.count {
let profile = candidates[idx]
if !selectedKeys.contains(profile.uniqueKey) {
selected.append(profile)
selectedKeys.insert(profile.uniqueKey)
addedAny = true
}
indices[key] = idx + 1
}
}
if !addedAny { break }
}
// Pass 6: If still need more, add remaining sorted by efficiency
if selected.count < maxCount {
let stillRemaining = uniqueProfiles
.filter { !selectedKeys.contains($0.uniqueKey) }
.sorted { efficiency(for: $0) > efficiency(for: $1) }
for profile in stillRemaining.prefix(maxCount - selected.count) {
selected.append(profile)
}
}
return selected.map { $0.route }
}
/// Diversity-aware pruning during beam expansion.
/// Keeps routes that span the diversity space rather than just high-scoring ones.
private static func diversityPrune(
_ paths: [[Game]],
stadiums: [UUID: Stadium],
targetCount: Int
) -> [[Game]] {
// Remove exact duplicates first
var uniquePaths: [[Game]] = []
var seen = Set<String>()
for path in paths {
let key = path.map { $0.id.uuidString }.joined(separator: "-")
if !seen.contains(key) {
seen.insert(key)
uniquePaths.append(path)
}
}
guard uniquePaths.count > targetCount else { return uniquePaths }
// Build profiles
let profiles = uniquePaths.map { buildProfile(for: $0, stadiums: stadiums) }
// Group by game count to ensure length diversity
let byGames = Dictionary(grouping: profiles) { $0.gameBucket }
let slotsPerBucket = max(2, targetCount / max(1, byGames.count))
var selected: [RouteProfile] = []
var selectedKeys = Set<String>()
// Take from each game count bucket
for bucket in byGames.keys.sorted() {
if let candidates = byGames[bucket] {
// Within bucket, prioritize geographic diversity
let byCities = Dictionary(grouping: candidates) { $0.cityBucket }
var bucketSelected = 0
for cityBucket in byCities.keys.sorted() {
if bucketSelected >= slotsPerBucket { break }
if let cityCandidates = byCities[cityBucket] {
for profile in cityCandidates.prefix(2) {
if !selectedKeys.contains(profile.uniqueKey) {
selected.append(profile)
selectedKeys.insert(profile.uniqueKey)
bucketSelected += 1
if bucketSelected >= slotsPerBucket { break }
}
}
}
}
}
}
// Fill remaining with efficiency-sorted paths
if selected.count < targetCount {
let remaining = profiles.filter { !selectedKeys.contains($0.uniqueKey) }
.sorted { efficiency(for: $0) > efficiency(for: $1) }
for profile in remaining.prefix(targetCount - selected.count) {
selected.append(profile)
}
}
return selected.map { $0.route }
}
/// Builds a profile for a route.
private static func buildProfile(for route: [Game], stadiums: [UUID: Stadium]) -> RouteProfile {
let gameCount = route.count
let cities = Set(route.compactMap { stadiums[$0.stadiumId]?.city })
let cityCount = cities.count
// Calculate total miles
var totalMiles: Double = 0
for i in 0..<(route.count - 1) {
totalMiles += estimateDistanceMiles(from: route[i], to: route[i + 1], stadiums: stadiums)
}
// Calculate trip duration in days
let tripDays: Int
if let firstGame = route.first, let lastGame = route.last {
let calendar = Calendar.current
let days = calendar.dateComponents([.day], from: firstGame.startTime, to: lastGame.startTime).day ?? 1
tripDays = max(1, days + 1)
} else {
tripDays = 1
}
return RouteProfile(
route: route,
gameCount: gameCount,
cityCount: cityCount,
totalMiles: totalMiles,
tripDays: tripDays
)
}
/// Calculates efficiency score (games per hour of driving).
private static func efficiency(for profile: RouteProfile) -> Double {
let drivingHours = profile.totalMiles / 60.0 // 60 mph average
guard drivingHours > 0 else { return Double(profile.gameCount) * 100 }
return Double(profile.gameCount) / drivingHours
}
// MARK: - Day Bucketing
/// Groups games by calendar day index (0 = first day of trip, 1 = second day, etc.)
private static func bucketByDay(games: [Game]) -> [Int: [Game]] {
guard let firstGame = games.first else { return [:] }
let referenceDate = firstGame.startTime
@@ -211,24 +451,15 @@ enum GameDAGRouter {
return buckets
}
/// Calculates the day index for a date relative to a reference date.
private static func dayIndexFor(_ date: Date, referenceDate: Date) -> Int {
let calendar = Calendar.current
let refDay = calendar.startOfDay(for: referenceDate)
let dateDay = calendar.startOfDay(for: date)
let components = calendar.dateComponents([.day], from: refDay, to: dateDay)
return components.day ?? 0
return calendar.dateComponents([.day], from: refDay, to: dateDay).day ?? 0
}
// MARK: - Transition Feasibility
/// Determines if we can travel from game A to game B.
///
/// Requirements:
/// 1. B starts after A (time moves forward)
/// 2. We have enough days between games to complete the drive
/// 3. We can arrive at B before B starts
///
private static func canTransition(
from: Game,
to: Game,
@@ -236,289 +467,62 @@ enum GameDAGRouter {
constraints: DrivingConstraints
) -> Bool {
// Time must move forward
guard to.startTime > from.startTime else {
return false
}
guard to.startTime > from.startTime else { return false }
// Same stadium = always feasible (no driving needed)
// Same stadium = always feasible
if from.stadiumId == to.stadiumId { return true }
// Get stadiums
guard let fromStadium = stadiums[from.stadiumId],
let toStadium = stadiums[to.stadiumId] else {
// Missing stadium info - can't calculate distance, reject to be safe
print("⚠️ DAG: Stadium lookup failed - from:\(stadiums[from.stadiumId] != nil) to:\(stadiums[to.stadiumId] != nil)")
return false
}
let fromCoord = fromStadium.coordinate
let toCoord = toStadium.coordinate
// Calculate driving time
let distanceMiles = TravelEstimator.haversineDistanceMiles(
from: CLLocationCoordinate2D(latitude: fromCoord.latitude, longitude: fromCoord.longitude),
to: CLLocationCoordinate2D(latitude: toCoord.latitude, longitude: toCoord.longitude)
from: CLLocationCoordinate2D(latitude: fromStadium.coordinate.latitude, longitude: fromStadium.coordinate.longitude),
to: CLLocationCoordinate2D(latitude: toStadium.coordinate.latitude, longitude: toStadium.coordinate.longitude)
) * 1.3 // Road routing factor
let drivingHours = distanceMiles / 60.0 // Average 60 mph
let drivingHours = distanceMiles / 60.0
// Calculate available driving time between games
// After game A ends (+ buffer), how much time until game B starts (- buffer)?
// Calculate available time
let departureTime = from.startTime.addingTimeInterval(gameEndBufferHours * 3600)
let deadline = to.startTime.addingTimeInterval(-3600) // 1 hour buffer before game
let availableSeconds = deadline.timeIntervalSince(departureTime)
let availableHours = availableSeconds / 3600.0
let availableHours = deadline.timeIntervalSince(departureTime) / 3600.0
// Calculate how many driving days we have
// Each day can have maxDailyDrivingHours of driving
// Calculate driving days available
let calendar = Calendar.current
let fromDay = calendar.startOfDay(for: from.startTime)
let toDay = calendar.startOfDay(for: to.startTime)
let daysBetween = calendar.dateComponents([.day], from: fromDay, to: toDay).day ?? 0
let daysBetween = calendar.dateComponents(
[.day],
from: calendar.startOfDay(for: from.startTime),
to: calendar.startOfDay(for: to.startTime)
).day ?? 0
// Available driving hours = days between * max per day
// (If games are same day, daysBetween = 0, but we might still have hours available)
let maxDrivingHoursAvailable: Double
if daysBetween == 0 {
// Same day - only have hours between games
maxDrivingHoursAvailable = max(0, availableHours)
} else {
// Multi-day - can drive each day
maxDrivingHoursAvailable = Double(daysBetween) * constraints.maxDailyDrivingHours
}
let maxDrivingHoursAvailable = daysBetween == 0
? max(0, availableHours)
: Double(daysBetween) * constraints.maxDailyDrivingHours
// Check if we have enough driving time
guard drivingHours <= maxDrivingHoursAvailable else {
return false
}
// Also verify we can arrive before game starts (sanity check)
guard availableHours >= drivingHours else {
return false
}
return true
return drivingHours <= maxDrivingHoursAvailable && drivingHours <= availableHours
}
// MARK: - Geographic Diversity
// MARK: - Distance Estimation
/// Selects diverse routes from the candidate set.
/// Ensures diversity by BOTH route length (city count) AND primary city.
/// This guarantees users see 2-city trips alongside 5+ city trips.
private static func selectDiverseRoutes(
_ routes: [[Game]],
stadiums: [UUID: Stadium],
maxCount: Int
) -> [[Game]] {
guard !routes.isEmpty else { return [] }
// Group routes by city count (route length)
var routesByLength: [Int: [[Game]]] = [:]
for route in routes {
let cityCount = Set(route.compactMap { stadiums[$0.stadiumId]?.city }).count
routesByLength[cityCount, default: []].append(route)
}
// Sort routes within each length by score
for (length, lengthRoutes) in routesByLength {
routesByLength[length] = lengthRoutes.sorted {
scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums)
}
}
// Allocate slots to each length category
// Goal: ensure at least 1 route per length category if available
let sortedLengths = routesByLength.keys.sorted()
let minPerLength = max(1, maxCount / max(1, sortedLengths.count))
var selectedRoutes: [[Game]] = []
var selectedIds = Set<String>()
// First pass: take best route(s) from each length category
for length in sortedLengths {
if selectedRoutes.count >= maxCount { break }
if let lengthRoutes = routesByLength[length] {
let toTake = min(minPerLength, lengthRoutes.count, maxCount - selectedRoutes.count)
for route in lengthRoutes.prefix(toTake) {
let key = route.map { $0.id.uuidString }.joined(separator: "-")
if !selectedIds.contains(key) {
selectedRoutes.append(route)
selectedIds.insert(key)
}
}
}
}
// Second pass: fill remaining slots, prioritizing geographic diversity
if selectedRoutes.count < maxCount {
// Group remaining routes by primary city
var remainingByCity: [String: [[Game]]] = [:]
for route in routes {
let key = route.map { $0.id.uuidString }.joined(separator: "-")
if !selectedIds.contains(key) {
let city = getPrimaryCity(for: route, stadiums: stadiums)
remainingByCity[city, default: []].append(route)
}
}
// Sort by score within each city
for (city, cityRoutes) in remainingByCity {
remainingByCity[city] = cityRoutes.sorted {
scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums)
}
}
// Round-robin from each city
let sortedCities = remainingByCity.keys.sorted { city1, city2 in
let score1 = remainingByCity[city1]?.first.map { scorePath($0, stadiums: stadiums) } ?? 0
let score2 = remainingByCity[city2]?.first.map { scorePath($0, stadiums: stadiums) } ?? 0
return score1 > score2
}
var cityIndices: [String: Int] = [:]
while selectedRoutes.count < maxCount {
var addedAny = false
for city in sortedCities {
if selectedRoutes.count >= maxCount { break }
let idx = cityIndices[city] ?? 0
if let cityRoutes = remainingByCity[city], idx < cityRoutes.count {
let route = cityRoutes[idx]
let key = route.map { $0.id.uuidString }.joined(separator: "-")
if !selectedIds.contains(key) {
selectedRoutes.append(route)
selectedIds.insert(key)
addedAny = true
}
cityIndices[city] = idx + 1
}
}
if !addedAny { break }
}
}
return selectedRoutes
}
/// Gets the primary city for a route (where most games are played).
private static func getPrimaryCity(for route: [Game], stadiums: [UUID: Stadium]) -> String {
var cityCounts: [String: Int] = [:]
for game in route {
let city = stadiums[game.stadiumId]?.city ?? "Unknown"
cityCounts[city, default: 0] += 1
}
return cityCounts.max(by: { $0.value < $1.value })?.key ?? "Unknown"
}
// MARK: - Scoring and Pruning
/// Scores a path. Higher = better.
/// Prefers: more games, less driving, geographic coherence
private static func scorePath(_ path: [Game], stadiums: [UUID: Stadium]) -> Double {
// Handle empty or single-game paths
guard path.count > 1 else {
return Double(path.count) * 100.0
}
let gameCount = Double(path.count)
// Calculate total driving
var totalDriving: Double = 0
for i in 0..<(path.count - 1) {
totalDriving += estimateDrivingHours(from: path[i], to: path[i + 1], stadiums: stadiums)
}
// Score: heavily weight game count, penalize driving
return gameCount * 100.0 - totalDriving * 2.0
}
/// Estimates driving hours between two games.
private static func estimateDrivingHours(
private static func estimateDistanceMiles(
from: Game,
to: Game,
stadiums: [UUID: Stadium]
) -> Double {
// Same stadium = 0 driving
if from.stadiumId == to.stadiumId { return 0 }
guard let fromStadium = stadiums[from.stadiumId],
let toStadium = stadiums[to.stadiumId] else {
return 5.0 // Fallback: assume 5 hours
return 300 // Fallback estimate
}
let fromCoord = fromStadium.coordinate
let toCoord = toStadium.coordinate
let distanceMiles = TravelEstimator.haversineDistanceMiles(
from: CLLocationCoordinate2D(latitude: fromCoord.latitude, longitude: fromCoord.longitude),
to: CLLocationCoordinate2D(latitude: toCoord.latitude, longitude: toCoord.longitude)
return TravelEstimator.haversineDistanceMiles(
from: CLLocationCoordinate2D(latitude: fromStadium.coordinate.latitude, longitude: fromStadium.coordinate.longitude),
to: CLLocationCoordinate2D(latitude: toStadium.coordinate.latitude, longitude: toStadium.coordinate.longitude)
) * 1.3
return distanceMiles / 60.0
}
/// Prunes dominated paths and truncates to beam width.
/// Maintains diversity by both ending city AND route length to ensure short trips aren't eliminated.
private static func pruneAndTruncate(
_ paths: [[Game]],
beamWidth: Int,
stadiums: [UUID: Stadium]
) -> [[Game]] {
// Remove exact duplicates
var uniquePaths: [[Game]] = []
var seen = Set<String>()
for path in paths {
let key = path.map { $0.id.uuidString }.joined(separator: "-")
if !seen.contains(key) {
seen.insert(key)
uniquePaths.append(path)
}
}
// Group paths by unique city count (route length)
// This ensures we keep short trips (2 cities) alongside long trips (5+ cities)
var pathsByLength: [Int: [[Game]]] = [:]
for path in uniquePaths {
let cityCount = Set(path.compactMap { stadiums[$0.stadiumId]?.city }).count
pathsByLength[cityCount, default: []].append(path)
}
// Sort paths within each length group by score
for (length, lengthPaths) in pathsByLength {
pathsByLength[length] = lengthPaths.sorted {
scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums)
}
}
// Allocate beam slots proportionally to length groups, with minimum per group
let sortedLengths = pathsByLength.keys.sorted()
let minPerLength = max(2, beamWidth / max(1, sortedLengths.count))
var pruned: [[Game]] = []
// First pass: take minimum from each length group
for length in sortedLengths {
if let lengthPaths = pathsByLength[length] {
let toTake = min(minPerLength, lengthPaths.count)
pruned.append(contentsOf: lengthPaths.prefix(toTake))
}
}
// Second pass: fill remaining slots with best paths overall
if pruned.count < beamWidth {
let remaining = beamWidth - pruned.count
let prunedIds = Set(pruned.map { $0.map { $0.id.uuidString }.joined(separator: "-") })
// Get all paths not yet added, sorted by score
var additional = uniquePaths.filter {
!prunedIds.contains($0.map { $0.id.uuidString }.joined(separator: "-"))
}
additional.sort { scorePath($0, stadiums: stadiums) > scorePath($1, stadiums: stadiums) }
pruned.append(contentsOf: additional.prefix(remaining))
}
// Final truncation
return Array(pruned.prefix(beamWidth))
}
}

View File

@@ -1286,5 +1286,789 @@
"WPG"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_gateway_center_arena",
"name": "Gateway Center Arena",
"city": "College Park",
"state": "GA",
"latitude": 33.6534,
"longitude": -84.448,
"capacity": 3500,
"sport": "WNBA",
"primary_team_abbrevs": [
"ATL"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_wintrust_arena",
"name": "Wintrust Arena",
"city": "Chicago",
"state": "IL",
"latitude": 41.8622,
"longitude": -87.6164,
"capacity": 10387,
"sport": "WNBA",
"primary_team_abbrevs": [
"CHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_mohegan_sun_arena",
"name": "Mohegan Sun Arena",
"city": "Uncasville",
"state": "CT",
"latitude": 41.4946,
"longitude": -72.0874,
"capacity": 10000,
"sport": "WNBA",
"primary_team_abbrevs": [
"CON"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_college_park_center",
"name": "College Park Center",
"city": "Arlington",
"state": "TX",
"latitude": 32.7298,
"longitude": -97.1137,
"capacity": 7000,
"sport": "WNBA",
"primary_team_abbrevs": [
"DAL"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_chase_center",
"name": "Chase Center",
"city": "San Francisco",
"state": "CA",
"latitude": 37.768,
"longitude": -122.3879,
"capacity": 18064,
"sport": "WNBA",
"primary_team_abbrevs": [
"GSV"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_gainbridge_fieldhouse",
"name": "Gainbridge Fieldhouse",
"city": "Indianapolis",
"state": "IN",
"latitude": 39.764,
"longitude": -86.1555,
"capacity": 17274,
"sport": "WNBA",
"primary_team_abbrevs": [
"IND"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_michelob_ultra_arena",
"name": "Michelob Ultra Arena",
"city": "Las Vegas",
"state": "NV",
"latitude": 36.0929,
"longitude": -115.1757,
"capacity": 12000,
"sport": "WNBA",
"primary_team_abbrevs": [
"LVA"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_cryptocom_arena",
"name": "Crypto.com Arena",
"city": "Los Angeles",
"state": "CA",
"latitude": 34.043,
"longitude": -118.2673,
"capacity": 19068,
"sport": "WNBA",
"primary_team_abbrevs": [
"LAS"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_target_center",
"name": "Target Center",
"city": "Minneapolis",
"state": "MN",
"latitude": 44.9795,
"longitude": -93.2761,
"capacity": 17500,
"sport": "WNBA",
"primary_team_abbrevs": [
"MIN"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_barclays_center",
"name": "Barclays Center",
"city": "Brooklyn",
"state": "NY",
"latitude": 40.6826,
"longitude": -73.9754,
"capacity": 17732,
"sport": "WNBA",
"primary_team_abbrevs": [
"NYL"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_footprint_center",
"name": "Footprint Center",
"city": "Phoenix",
"state": "AZ",
"latitude": 33.4457,
"longitude": -112.0712,
"capacity": 17000,
"sport": "WNBA",
"primary_team_abbrevs": [
"PHX"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_climate_pledge_arena",
"name": "Climate Pledge Arena",
"city": "Seattle",
"state": "WA",
"latitude": 47.6221,
"longitude": -122.354,
"capacity": 17100,
"sport": "WNBA",
"primary_team_abbrevs": [
"SEA"
],
"year_opened": null
},
{
"canonical_id": "stadium_wnba_entertainment__sports_arena",
"name": "Entertainment & Sports Arena",
"city": "Washington",
"state": "DC",
"latitude": 38.8701,
"longitude": -76.9728,
"capacity": 4200,
"sport": "WNBA",
"primary_team_abbrevs": [
"WAS"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_mercedes-benz_stadium",
"name": "Mercedes-Benz Stadium",
"city": "Atlanta",
"state": "GA",
"latitude": 33.7553,
"longitude": -84.4006,
"capacity": 71000,
"sport": "MLS",
"primary_team_abbrevs": [
"ATL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_q2_stadium",
"name": "Q2 Stadium",
"city": "Austin",
"state": "TX",
"latitude": 30.3876,
"longitude": -97.72,
"capacity": 20738,
"sport": "MLS",
"primary_team_abbrevs": [
"ATX"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bank_of_america_stadium",
"name": "Bank of America Stadium",
"city": "Charlotte",
"state": "NC",
"latitude": 35.2258,
"longitude": -80.8528,
"capacity": 74867,
"sport": "MLS",
"primary_team_abbrevs": [
"CLT"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_soldier_field",
"name": "Soldier Field",
"city": "Chicago",
"state": "IL",
"latitude": 41.8623,
"longitude": -87.6167,
"capacity": 61500,
"sport": "MLS",
"primary_team_abbrevs": [
"CHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_tql_stadium",
"name": "TQL Stadium",
"city": "Cincinnati",
"state": "OH",
"latitude": 39.1113,
"longitude": -84.5212,
"capacity": 26000,
"sport": "MLS",
"primary_team_abbrevs": [
"CIN"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_dicks_sporting_goods_park",
"name": "Dicks Sporting Goods Park",
"city": "Commerce City",
"state": "CO",
"latitude": 39.8056,
"longitude": -104.8919,
"capacity": 18061,
"sport": "MLS",
"primary_team_abbrevs": [
"COL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_lowercom_field",
"name": "Lower.com Field",
"city": "Columbus",
"state": "OH",
"latitude": 39.9689,
"longitude": -83.0173,
"capacity": 20371,
"sport": "MLS",
"primary_team_abbrevs": [
"CLB"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_toyota_stadium",
"name": "Toyota Stadium",
"city": "Frisco",
"state": "TX",
"latitude": 33.1546,
"longitude": -96.8353,
"capacity": 20500,
"sport": "MLS",
"primary_team_abbrevs": [
"DAL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_audi_field",
"name": "Audi Field",
"city": "Washington",
"state": "DC",
"latitude": 38.8686,
"longitude": -77.0128,
"capacity": 20000,
"sport": "MLS",
"primary_team_abbrevs": [
"DCU"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_shell_energy_stadium",
"name": "Shell Energy Stadium",
"city": "Houston",
"state": "TX",
"latitude": 29.7523,
"longitude": -95.3522,
"capacity": 22039,
"sport": "MLS",
"primary_team_abbrevs": [
"HOU"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_dignity_health_sports_park",
"name": "Dignity Health Sports Park",
"city": "Carson",
"state": "CA",
"latitude": 33.8644,
"longitude": -118.2611,
"capacity": 27000,
"sport": "MLS",
"primary_team_abbrevs": [
"LAG"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bmo_stadium",
"name": "BMO Stadium",
"city": "Los Angeles",
"state": "CA",
"latitude": 34.0128,
"longitude": -118.2841,
"capacity": 22000,
"sport": "MLS",
"primary_team_abbrevs": [
"LAFC"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_chase_stadium",
"name": "Chase Stadium",
"city": "Fort Lauderdale",
"state": "FL",
"latitude": 26.1902,
"longitude": -80.163,
"capacity": 21550,
"sport": "MLS",
"primary_team_abbrevs": [
"MIA"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_allianz_field",
"name": "Allianz Field",
"city": "St. Paul",
"state": "MN",
"latitude": 44.9532,
"longitude": -93.1653,
"capacity": 19400,
"sport": "MLS",
"primary_team_abbrevs": [
"MIN"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_stade_saputo",
"name": "Stade Saputo",
"city": "Montreal",
"state": "QC",
"latitude": 45.5628,
"longitude": -73.553,
"capacity": 19619,
"sport": "MLS",
"primary_team_abbrevs": [
"MTL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_geodis_park",
"name": "Geodis Park",
"city": "Nashville",
"state": "TN",
"latitude": 36.1303,
"longitude": -86.7663,
"capacity": 30000,
"sport": "MLS",
"primary_team_abbrevs": [
"NSH"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_gillette_stadium",
"name": "Gillette Stadium",
"city": "Foxborough",
"state": "MA",
"latitude": 42.0909,
"longitude": -71.2643,
"capacity": 65878,
"sport": "MLS",
"primary_team_abbrevs": [
"NER"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_yankee_stadium",
"name": "Yankee Stadium",
"city": "New York",
"state": "NY",
"latitude": 40.8296,
"longitude": -73.9262,
"capacity": 46537,
"sport": "MLS",
"primary_team_abbrevs": [
"NYC"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_red_bull_arena",
"name": "Red Bull Arena",
"city": "Harrison",
"state": "NJ",
"latitude": 40.7368,
"longitude": -74.1503,
"capacity": 25000,
"sport": "MLS",
"primary_team_abbrevs": [
"RBNY"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_interco_stadium",
"name": "InterCo Stadium",
"city": "Orlando",
"state": "FL",
"latitude": 28.5411,
"longitude": -81.3899,
"capacity": 25500,
"sport": "MLS",
"primary_team_abbrevs": [
"ORL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_subaru_park",
"name": "Subaru Park",
"city": "Chester",
"state": "PA",
"latitude": 39.8328,
"longitude": -75.3789,
"capacity": 18500,
"sport": "MLS",
"primary_team_abbrevs": [
"PHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_providence_park",
"name": "Providence Park",
"city": "Portland",
"state": "OR",
"latitude": 45.5217,
"longitude": -122.6917,
"capacity": 25218,
"sport": "MLS",
"primary_team_abbrevs": [
"POR"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_america_first_field",
"name": "America First Field",
"city": "Sandy",
"state": "UT",
"latitude": 40.5828,
"longitude": -111.8933,
"capacity": 20213,
"sport": "MLS",
"primary_team_abbrevs": [
"RSL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_paypal_park",
"name": "PayPal Park",
"city": "San Jose",
"state": "CA",
"latitude": 37.3513,
"longitude": -121.9253,
"capacity": 18000,
"sport": "MLS",
"primary_team_abbrevs": [
"SJE"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_lumen_field",
"name": "Lumen Field",
"city": "Seattle",
"state": "WA",
"latitude": 47.5952,
"longitude": -122.3316,
"capacity": 68740,
"sport": "MLS",
"primary_team_abbrevs": [
"SEA"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_childrens_mercy_park",
"name": "Childrens Mercy Park",
"city": "Kansas City",
"state": "KS",
"latitude": 39.1218,
"longitude": -94.8234,
"capacity": 18467,
"sport": "MLS",
"primary_team_abbrevs": [
"SKC"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_citypark",
"name": "CityPark",
"city": "St. Louis",
"state": "MO",
"latitude": 38.6322,
"longitude": -90.2094,
"capacity": 22500,
"sport": "MLS",
"primary_team_abbrevs": [
"STL"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bmo_field",
"name": "BMO Field",
"city": "Toronto",
"state": "ON",
"latitude": 43.6332,
"longitude": -79.4186,
"capacity": 30000,
"sport": "MLS",
"primary_team_abbrevs": [
"TOR"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_bc_place",
"name": "BC Place",
"city": "Vancouver",
"state": "BC",
"latitude": 49.2768,
"longitude": -123.1118,
"capacity": 54320,
"sport": "MLS",
"primary_team_abbrevs": [
"VAN"
],
"year_opened": null
},
{
"canonical_id": "stadium_mls_snapdragon_stadium",
"name": "Snapdragon Stadium",
"city": "San Diego",
"state": "CA",
"latitude": 32.7839,
"longitude": -117.1224,
"capacity": 35000,
"sport": "MLS",
"primary_team_abbrevs": [
"SDG"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_bmo_stadium",
"name": "BMO Stadium",
"city": "Los Angeles",
"state": "CA",
"latitude": 34.0128,
"longitude": -118.2841,
"capacity": 22000,
"sport": "NWSL",
"primary_team_abbrevs": [
"ANG"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_paypal_park",
"name": "PayPal Park",
"city": "San Jose",
"state": "CA",
"latitude": 37.3513,
"longitude": -121.9253,
"capacity": 18000,
"sport": "NWSL",
"primary_team_abbrevs": [
"BAY"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_seatgeek_stadium",
"name": "SeatGeek Stadium",
"city": "Chicago",
"state": "IL",
"latitude": 41.6462,
"longitude": -87.7304,
"capacity": 20000,
"sport": "NWSL",
"primary_team_abbrevs": [
"CHI"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_shell_energy_stadium",
"name": "Shell Energy Stadium",
"city": "Houston",
"state": "TX",
"latitude": 29.7523,
"longitude": -95.3522,
"capacity": 22039,
"sport": "NWSL",
"primary_team_abbrevs": [
"HOU"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_cpkc_stadium",
"name": "CPKC Stadium",
"city": "Kansas City",
"state": "KS",
"latitude": 39.0851,
"longitude": -94.5582,
"capacity": 11500,
"sport": "NWSL",
"primary_team_abbrevs": [
"KCC"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_red_bull_arena",
"name": "Red Bull Arena",
"city": "Harrison",
"state": "NJ",
"latitude": 40.7368,
"longitude": -74.1503,
"capacity": 25000,
"sport": "NWSL",
"primary_team_abbrevs": [
"NJY"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_wakemed_soccer_park",
"name": "WakeMed Soccer Park",
"city": "Cary",
"state": "NC",
"latitude": 35.8589,
"longitude": -78.7989,
"capacity": 10000,
"sport": "NWSL",
"primary_team_abbrevs": [
"NCC"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_interco_stadium",
"name": "InterCo Stadium",
"city": "Orlando",
"state": "FL",
"latitude": 28.5411,
"longitude": -81.3899,
"capacity": 25500,
"sport": "NWSL",
"primary_team_abbrevs": [
"ORL"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_providence_park",
"name": "Providence Park",
"city": "Portland",
"state": "OR",
"latitude": 45.5217,
"longitude": -122.6917,
"capacity": 25218,
"sport": "NWSL",
"primary_team_abbrevs": [
"POR"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_lumen_field",
"name": "Lumen Field",
"city": "Seattle",
"state": "WA",
"latitude": 47.5952,
"longitude": -122.3316,
"capacity": 68740,
"sport": "NWSL",
"primary_team_abbrevs": [
"RGN"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_snapdragon_stadium",
"name": "Snapdragon Stadium",
"city": "San Diego",
"state": "CA",
"latitude": 32.7839,
"longitude": -117.1224,
"capacity": 35000,
"sport": "NWSL",
"primary_team_abbrevs": [
"SDW"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_america_first_field",
"name": "America First Field",
"city": "Sandy",
"state": "UT",
"latitude": 40.5828,
"longitude": -111.8933,
"capacity": 20213,
"sport": "NWSL",
"primary_team_abbrevs": [
"UTA"
],
"year_opened": null
},
{
"canonical_id": "stadium_nwsl_audi_field",
"name": "Audi Field",
"city": "Washington",
"state": "DC",
"latitude": 38.8686,
"longitude": -77.0128,
"capacity": 20000,
"sport": "NWSL",
"primary_team_abbrevs": [
"WSH"
],
"year_opened": null
}
]

View File

@@ -1102,5 +1102,677 @@
"division_id": "nhl_central",
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_atl",
"name": "Atlanta Dream",
"abbreviation": "ATL",
"sport": "WNBA",
"city": "College Park",
"stadium_canonical_id": "stadium_wnba_gateway_center_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_chi",
"name": "Chicago Sky",
"abbreviation": "CHI",
"sport": "WNBA",
"city": "Chicago",
"stadium_canonical_id": "stadium_wnba_wintrust_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_con",
"name": "Connecticut Sun",
"abbreviation": "CON",
"sport": "WNBA",
"city": "Uncasville",
"stadium_canonical_id": "stadium_wnba_mohegan_sun_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_dal",
"name": "Dallas Wings",
"abbreviation": "DAL",
"sport": "WNBA",
"city": "Arlington",
"stadium_canonical_id": "stadium_wnba_college_park_center",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_gsv",
"name": "Golden State Valkyries",
"abbreviation": "GSV",
"sport": "WNBA",
"city": "San Francisco",
"stadium_canonical_id": "stadium_wnba_chase_center",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_ind",
"name": "Indiana Fever",
"abbreviation": "IND",
"sport": "WNBA",
"city": "Indianapolis",
"stadium_canonical_id": "stadium_wnba_gainbridge_fieldhouse",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_lva",
"name": "Las Vegas Aces",
"abbreviation": "LVA",
"sport": "WNBA",
"city": "Las Vegas",
"stadium_canonical_id": "stadium_wnba_michelob_ultra_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_las",
"name": "Los Angeles Sparks",
"abbreviation": "LAS",
"sport": "WNBA",
"city": "Los Angeles",
"stadium_canonical_id": "stadium_wnba_cryptocom_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_min",
"name": "Minnesota Lynx",
"abbreviation": "MIN",
"sport": "WNBA",
"city": "Minneapolis",
"stadium_canonical_id": "stadium_wnba_target_center",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_nyl",
"name": "New York Liberty",
"abbreviation": "NYL",
"sport": "WNBA",
"city": "Brooklyn",
"stadium_canonical_id": "stadium_wnba_barclays_center",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_phx",
"name": "Phoenix Mercury",
"abbreviation": "PHX",
"sport": "WNBA",
"city": "Phoenix",
"stadium_canonical_id": "stadium_wnba_footprint_center",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_sea",
"name": "Seattle Storm",
"abbreviation": "SEA",
"sport": "WNBA",
"city": "Seattle",
"stadium_canonical_id": "stadium_wnba_climate_pledge_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_wnba_was",
"name": "Washington Mystics",
"abbreviation": "WAS",
"sport": "WNBA",
"city": "Washington",
"stadium_canonical_id": "stadium_wnba_entertainment__sports_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_atl",
"name": "Atlanta United FC",
"abbreviation": "ATL",
"sport": "MLS",
"city": "Atlanta",
"stadium_canonical_id": "stadium_mls_mercedes-benz_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_atx",
"name": "Austin FC",
"abbreviation": "ATX",
"sport": "MLS",
"city": "Austin",
"stadium_canonical_id": "stadium_mls_q2_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_clt",
"name": "Charlotte FC",
"abbreviation": "CLT",
"sport": "MLS",
"city": "Charlotte",
"stadium_canonical_id": "stadium_mls_bank_of_america_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_chi",
"name": "Chicago Fire FC",
"abbreviation": "CHI",
"sport": "MLS",
"city": "Chicago",
"stadium_canonical_id": "stadium_mls_soldier_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_cin",
"name": "FC Cincinnati",
"abbreviation": "CIN",
"sport": "MLS",
"city": "Cincinnati",
"stadium_canonical_id": "stadium_mls_tql_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_col",
"name": "Colorado Rapids",
"abbreviation": "COL",
"sport": "MLS",
"city": "Commerce City",
"stadium_canonical_id": "stadium_mls_dicks_sporting_goods_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_clb",
"name": "Columbus Crew",
"abbreviation": "CLB",
"sport": "MLS",
"city": "Columbus",
"stadium_canonical_id": "stadium_mls_lowercom_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_dal",
"name": "FC Dallas",
"abbreviation": "DAL",
"sport": "MLS",
"city": "Frisco",
"stadium_canonical_id": "stadium_mls_toyota_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_dcu",
"name": "D.C. United",
"abbreviation": "DCU",
"sport": "MLS",
"city": "Washington",
"stadium_canonical_id": "stadium_mls_audi_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_hou",
"name": "Houston Dynamo FC",
"abbreviation": "HOU",
"sport": "MLS",
"city": "Houston",
"stadium_canonical_id": "stadium_mls_shell_energy_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_lag",
"name": "LA Galaxy",
"abbreviation": "LAG",
"sport": "MLS",
"city": "Carson",
"stadium_canonical_id": "stadium_mls_dignity_health_sports_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_lafc",
"name": "Los Angeles FC",
"abbreviation": "LAFC",
"sport": "MLS",
"city": "Los Angeles",
"stadium_canonical_id": "stadium_mls_bmo_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_mia",
"name": "Inter Miami CF",
"abbreviation": "MIA",
"sport": "MLS",
"city": "Fort Lauderdale",
"stadium_canonical_id": "stadium_mls_chase_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_min",
"name": "Minnesota United FC",
"abbreviation": "MIN",
"sport": "MLS",
"city": "St. Paul",
"stadium_canonical_id": "stadium_mls_allianz_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_mtl",
"name": "CF Montreal",
"abbreviation": "MTL",
"sport": "MLS",
"city": "Montreal",
"stadium_canonical_id": "stadium_mls_stade_saputo",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_nsh",
"name": "Nashville SC",
"abbreviation": "NSH",
"sport": "MLS",
"city": "Nashville",
"stadium_canonical_id": "stadium_mls_geodis_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_ner",
"name": "New England Revolution",
"abbreviation": "NER",
"sport": "MLS",
"city": "Foxborough",
"stadium_canonical_id": "stadium_mls_gillette_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_nyc",
"name": "New York City FC",
"abbreviation": "NYC",
"sport": "MLS",
"city": "New York",
"stadium_canonical_id": "stadium_mls_yankee_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_rbny",
"name": "New York Red Bulls",
"abbreviation": "RBNY",
"sport": "MLS",
"city": "Harrison",
"stadium_canonical_id": "stadium_mls_red_bull_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_orl",
"name": "Orlando City SC",
"abbreviation": "ORL",
"sport": "MLS",
"city": "Orlando",
"stadium_canonical_id": "stadium_mls_interco_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_phi",
"name": "Philadelphia Union",
"abbreviation": "PHI",
"sport": "MLS",
"city": "Chester",
"stadium_canonical_id": "stadium_mls_subaru_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_por",
"name": "Portland Timbers",
"abbreviation": "POR",
"sport": "MLS",
"city": "Portland",
"stadium_canonical_id": "stadium_mls_providence_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_rsl",
"name": "Real Salt Lake",
"abbreviation": "RSL",
"sport": "MLS",
"city": "Sandy",
"stadium_canonical_id": "stadium_mls_america_first_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_sje",
"name": "San Jose Earthquakes",
"abbreviation": "SJE",
"sport": "MLS",
"city": "San Jose",
"stadium_canonical_id": "stadium_mls_paypal_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_sea",
"name": "Seattle Sounders FC",
"abbreviation": "SEA",
"sport": "MLS",
"city": "Seattle",
"stadium_canonical_id": "stadium_mls_lumen_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_skc",
"name": "Sporting Kansas City",
"abbreviation": "SKC",
"sport": "MLS",
"city": "Kansas City",
"stadium_canonical_id": "stadium_mls_childrens_mercy_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_stl",
"name": "St. Louis City SC",
"abbreviation": "STL",
"sport": "MLS",
"city": "St. Louis",
"stadium_canonical_id": "stadium_mls_citypark",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_tor",
"name": "Toronto FC",
"abbreviation": "TOR",
"sport": "MLS",
"city": "Toronto",
"stadium_canonical_id": "stadium_mls_bmo_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_van",
"name": "Vancouver Whitecaps FC",
"abbreviation": "VAN",
"sport": "MLS",
"city": "Vancouver",
"stadium_canonical_id": "stadium_mls_bc_place",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_mls_sdg",
"name": "San Diego FC",
"abbreviation": "SDG",
"sport": "MLS",
"city": "San Diego",
"stadium_canonical_id": "stadium_mls_snapdragon_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_ang",
"name": "Angel City FC",
"abbreviation": "ANG",
"sport": "NWSL",
"city": "Los Angeles",
"stadium_canonical_id": "stadium_nwsl_bmo_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_bay",
"name": "Bay FC",
"abbreviation": "BAY",
"sport": "NWSL",
"city": "San Jose",
"stadium_canonical_id": "stadium_nwsl_paypal_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_chi",
"name": "Chicago Red Stars",
"abbreviation": "CHI",
"sport": "NWSL",
"city": "Chicago",
"stadium_canonical_id": "stadium_nwsl_seatgeek_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_hou",
"name": "Houston Dash",
"abbreviation": "HOU",
"sport": "NWSL",
"city": "Houston",
"stadium_canonical_id": "stadium_nwsl_shell_energy_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_kcc",
"name": "Kansas City Current",
"abbreviation": "KCC",
"sport": "NWSL",
"city": "Kansas City",
"stadium_canonical_id": "stadium_nwsl_cpkc_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_njy",
"name": "NJ/NY Gotham FC",
"abbreviation": "NJY",
"sport": "NWSL",
"city": "Harrison",
"stadium_canonical_id": "stadium_nwsl_red_bull_arena",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_ncc",
"name": "North Carolina Courage",
"abbreviation": "NCC",
"sport": "NWSL",
"city": "Cary",
"stadium_canonical_id": "stadium_nwsl_wakemed_soccer_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_orl",
"name": "Orlando Pride",
"abbreviation": "ORL",
"sport": "NWSL",
"city": "Orlando",
"stadium_canonical_id": "stadium_nwsl_interco_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_por",
"name": "Portland Thorns FC",
"abbreviation": "POR",
"sport": "NWSL",
"city": "Portland",
"stadium_canonical_id": "stadium_nwsl_providence_park",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_rgn",
"name": "Seattle Reign FC",
"abbreviation": "RGN",
"sport": "NWSL",
"city": "Seattle",
"stadium_canonical_id": "stadium_nwsl_lumen_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_sdw",
"name": "San Diego Wave FC",
"abbreviation": "SDW",
"sport": "NWSL",
"city": "San Diego",
"stadium_canonical_id": "stadium_nwsl_snapdragon_stadium",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_uta",
"name": "Utah Royals FC",
"abbreviation": "UTA",
"sport": "NWSL",
"city": "Sandy",
"stadium_canonical_id": "stadium_nwsl_america_first_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
},
{
"canonical_id": "team_nwsl_wsh",
"name": "Washington Spirit",
"abbreviation": "WSH",
"sport": "NWSL",
"city": "Washington",
"stadium_canonical_id": "stadium_nwsl_audi_field",
"conference_id": null,
"division_id": null,
"primary_color": null,
"secondary_color": null
}
]

368
docs/DATA_SCRAPING.md Normal file
View File

@@ -0,0 +1,368 @@
# Data Scraping System
This document describes the SportsTime schedule scraping system, including all data sources, the fallback architecture, and operational procedures.
## Overview
The scraping system (`Scripts/scrape_schedules.py`) fetches game schedules for 8 sports leagues from multiple data sources. It uses a **multi-source fallback architecture** to ensure reliability—if one source fails or returns insufficient data, the system automatically tries backup sources.
## Supported Sports
| Sport | League | Season Format | Typical Games |
|-------|--------|---------------|---------------|
| NBA | National Basketball Association | 2024-25 | ~1,230 |
| MLB | Major League Baseball | 2025 | ~2,430 |
| NHL | National Hockey League | 2024-25 | ~1,312 |
| NFL | National Football League | 2025-26 | ~272 |
| WNBA | Women's National Basketball Association | 2025 | ~200 |
| MLS | Major League Soccer | 2025 | ~500 |
| NWSL | National Women's Soccer League | 2025 | ~180 |
| CBB | NCAA Division I Basketball | 2025-26 | ~5,000+ |
## Data Sources by Sport
Each sport has 3 data sources configured in priority order. The scraper tries sources sequentially until one returns sufficient data.
### NBA (National Basketball Association)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | Basketball-Reference | `basketball-reference.com/leagues/NBA_{year}_games-{month}.html` | 500 |
| 2 | ESPN API | `site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard` | 500 |
| 3 | CBS Sports | `cbssports.com/nba/schedule/` | 100 |
**Notes:**
- Basketball-Reference is most reliable for historical data
- ESPN API provides real-time updates but may have rate limits
- CBS Sports as emergency fallback
### MLB (Major League Baseball)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | MLB Stats API | `statsapi.mlb.com/api/v1/schedule` | 1,000 |
| 2 | Baseball-Reference | `baseball-reference.com/leagues/majors/{year}-schedule.shtml` | 500 |
| 3 | ESPN API | `site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard` | 500 |
**Notes:**
- MLB Stats API is official and most complete
- Baseball-Reference good for historical seasons
- Rate limit: 1 request/second for all sources
### NHL (National Hockey League)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | Hockey-Reference | `hockey-reference.com/leagues/NHL_{year}_games.html` | 500 |
| 2 | ESPN API | `site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard` | 500 |
| 3 | NHL API | `api-web.nhle.com/v1/schedule/{date}` | 100 |
**Notes:**
- Hockey-Reference uses season format like "2025" for 2024-25 season
- NHL API is official but documentation is limited
### NFL (National Football League)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | ESPN API | `site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard` | 200 |
| 2 | Pro-Football-Reference | `pro-football-reference.com/years/{year}/games.htm` | 200 |
| 3 | CBS Sports | `cbssports.com/nfl/schedule/` | 100 |
**Notes:**
- ESPN provides week-by-week schedule data
- PFR has complete historical archives
- Season runs September-February (crosses calendar years)
### WNBA (Women's National Basketball Association)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | ESPN API | `site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard` | 100 |
| 2 | Basketball-Reference | `basketball-reference.com/wnba/years/{year}_games.html` | 100 |
| 3 | CBS Sports | `cbssports.com/wnba/schedule/` | 50 |
**Notes:**
- WNBA season runs May-September
- Fewer games than NBA (12 teams, 40-game season)
### MLS (Major League Soccer)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | ESPN API | `site.api.espn.com/apis/site/v2/sports/soccer/usa.1/scoreboard` | 200 |
| 2 | FBref | `fbref.com/en/comps/22/{year}/schedule/` | 100 |
| 3 | MLSSoccer.com | `mlssoccer.com/schedule/scores` | 100 |
**Notes:**
- ESPN's league ID for MLS is `usa.1`
- FBref may block automated requests (403 errors)
- Season runs February-November
### NWSL (National Women's Soccer League)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | ESPN API | `site.api.espn.com/apis/site/v2/sports/soccer/usa.nwsl/scoreboard` | 100 |
| 2 | FBref | `fbref.com/en/comps/182/{year}/schedule/` | 50 |
| 3 | NWSL.com | `nwslsoccer.com/schedule` | 50 |
**Notes:**
- ESPN's league ID for NWSL is `usa.nwsl`
- 14 teams, ~180 regular season games
### CBB (College Basketball - Division I)
| Priority | Source | URL Pattern | Min Games |
|----------|--------|-------------|-----------|
| 1 | ESPN API | `site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard` | 1,000 |
| 2 | Sports-Reference | `sports-reference.com/cbb/seasons/{year}-schedule.html` | 500 |
| 3 | CBS Sports | `cbssports.com/college-basketball/schedule/` | 300 |
**Notes:**
- ~360 Division I teams = 5,000+ games per season
- ESPN provides group filtering (D1 = group 50)
- Season runs November-April (March Madness)
## Fallback Architecture
### ScraperSource Configuration
```python
@dataclass
class ScraperSource:
name: str # Display name (e.g., "ESPN")
scraper_func: Callable[[int], list] # Function taking season year
priority: int = 1 # Lower = higher priority
min_games: int = 10 # Minimum to consider success
```
### Fallback Logic
```python
def scrape_with_fallback(sport, season, sources):
sources = sorted(sources, key=lambda s: s.priority)
for source in sources:
try:
games = source.scraper_func(season)
if len(games) >= source.min_games:
return games # Success!
except Exception:
continue # Try next source
return [] # All sources failed
```
### Example Output
```
SCRAPING NBA 2026
============================================================
[1/3] Trying Basketball-Reference...
✓ Basketball-Reference returned 1230 games
SCRAPING MLB 2026
============================================================
[1/3] Trying MLB Stats API...
✗ MLB Stats API failed: Connection timeout
[2/3] Trying Baseball-Reference...
✓ Baseball-Reference returned 2430 games
```
## Usage
### Command Line Interface
```bash
# Scrape all sports for 2026 season
python scrape_schedules.py --sport all --season 2026
# Scrape specific sport
python scrape_schedules.py --sport nba --season 2026
python scrape_schedules.py --sport mlb --season 2026
# Scrape only stadiums (legacy method)
python scrape_schedules.py --stadiums-only
# Scrape comprehensive stadium data for ALL 11 sports
python scrape_schedules.py --stadiums-update
# Custom output directory
python scrape_schedules.py --sport all --season 2026 --output ./custom_data
```
### Available Options
| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| `--sport` | `nba`, `mlb`, `nhl`, `nfl`, `wnba`, `mls`, `nwsl`, `cbb`, `all` | `all` | Sport(s) to scrape |
| `--season` | Year (int) | `2026` | Season ending year |
| `--stadiums-only` | Flag | False | Only scrape stadium data (legacy method) |
| `--stadiums-update` | Flag | False | Scrape ALL stadium data for all 8 sports |
| `--output` | Path | `./data` | Output directory |
## Output Format
### Directory Structure
```
data/
├── games.json # All games from all sports
├── stadiums.json # All stadium/venue data
└── teams.json # Team metadata (generated)
```
### Game JSON Schema
```json
{
"id": "NBA-2025-26-LAL-BOS-20251225",
"sport": "NBA",
"homeTeam": "Los Angeles Lakers",
"awayTeam": "Boston Celtics",
"homeTeamId": "LAL",
"awayTeamId": "BOS",
"date": "2025-12-25T20:00:00Z",
"venue": "Crypto.com Arena",
"city": "Los Angeles",
"state": "CA"
}
```
### Stadium JSON Schema
```json
{
"id": "crypto-com-arena",
"name": "Crypto.com Arena",
"city": "Los Angeles",
"state": "CA",
"latitude": 34.0430,
"longitude": -118.2673,
"sports": ["NBA", "NHL"],
"teams": ["Los Angeles Lakers", "Los Angeles Kings", "Los Angeles Clippers"]
}
```
## Stable Game IDs
Games are assigned stable IDs using the pattern:
```
{SPORT}-{SEASON}-{AWAY}-{HOME}-{DATE}
```
Example: `NBA-2025-26-LAL-BOS-20251225`
This ensures:
- Same game gets same ID across scraper runs
- IDs survive if scraper source changes
- CloudKit records can be updated (not duplicated)
## Rate Limiting
All scrapers implement rate limiting to avoid being blocked:
| Source Type | Rate Limit | Implementation |
|-------------|------------|----------------|
| Sports-Reference family | 1 req/sec | `time.sleep(1)` between requests |
| ESPN API | 0.5 req/sec | `time.sleep(0.5)` between date ranges |
| Official APIs (MLB, NHL) | 1 req/sec | `time.sleep(1)` between requests |
| CBS Sports | 1 req/sec | `time.sleep(1)` between pages |
## Error Handling
### Common Errors
| Error | Cause | Resolution |
|-------|-------|------------|
| `403 Forbidden` | Rate limited or blocked | Wait 5 min, reduce request rate |
| `Connection timeout` | Network issue | Retry, check connectivity |
| `0 games returned` | Off-season or parsing error | Check if season has started |
| `KeyError` in parsing | Website structure changed | Update scraper selectors |
### Fallback Behavior
1. If primary source fails → Try source #2
2. If source #2 fails → Try source #3
3. If all sources fail → Log warning, return empty list
4. Script continues to next sport (doesn't abort)
## Adding New Sources
### 1. Create Scraper Function
```python
def scrape_newsport_newsource(season: int) -> list[Game]:
"""Scrape NewSport schedule from NewSource."""
games = []
url = f"https://newsource.com/schedule/{season}"
response = requests.get(url, headers=HEADERS)
# Parse response...
return games
```
### 2. Register in main()
```python
if args.sport in ['newsport', 'all']:
sources = [
ScraperSource('Primary', scrape_newsport_primary, priority=1, min_games=100),
ScraperSource('NewSource', scrape_newsport_newsource, priority=2, min_games=50),
ScraperSource('Backup', scrape_newsport_backup, priority=3, min_games=25),
]
games = scrape_with_fallback('NEWSPORT', args.season, sources)
```
### 3. Add to CLI choices
```python
parser.add_argument('--sport', choices=[..., 'newsport', 'all'])
```
## Maintenance
### Monthly Tasks
- Run full scrape to update schedules
- Check for 403 errors indicating blocked sources
- Verify game counts match expected totals
### Seasonal Tasks
- Update season year in scripts
- Check for website structure changes
- Verify new teams/venues are included
### When Sources Break
1. Check if website changed structure (inspect HTML)
2. Update CSS selectors or JSON paths
3. If permanently broken, add new backup source
4. Update min_games thresholds if needed
## Dependencies
```
requests>=2.28.0
beautifulsoup4>=4.11.0
lxml>=4.9.0
```
Install with:
```bash
cd Scripts && pip install -r requirements.txt
```
## CloudKit Integration
After scraping, data is uploaded to CloudKit via:
```bash
python cloudkit_import.py
```
This syncs:
- Games → `CanonicalGame` records
- Stadiums → `CanonicalStadium` records
- Teams → `CanonicalTeam` records
The iOS app then syncs from CloudKit to local SwiftData storage.

File diff suppressed because it is too large Load Diff