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:
+141
-31
@@ -7,24 +7,43 @@ Imports canonical JSON data into CloudKit. Run after canonicalization pipeline.
|
|||||||
Expected input files (from canonicalization pipeline):
|
Expected input files (from canonicalization pipeline):
|
||||||
- stadiums_canonical.json
|
- stadiums_canonical.json
|
||||||
- teams_canonical.json
|
- teams_canonical.json
|
||||||
- games_canonical.json
|
- games_canonical.json OR canonical/games/*.json (new structure)
|
||||||
- stadium_aliases.json
|
- stadium_aliases.json
|
||||||
- league_structure.json
|
- league_structure.json
|
||||||
- team_aliases.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:
|
Setup:
|
||||||
1. CloudKit Dashboard > Tokens & Keys > Server-to-Server Keys
|
1. CloudKit Dashboard > Tokens & Keys > Server-to-Server Keys
|
||||||
2. Create key with Read/Write access to public database
|
2. Create key with Read/Write access to public database
|
||||||
3. Download .p8 file and note Key ID
|
3. Download .p8 file and note Key ID
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python cloudkit_import.py --dry-run # Preview first
|
python cloudkit_import.py # Interactive menu
|
||||||
python cloudkit_import.py --key-id XX --key-file key.p8 # Import all
|
python cloudkit_import.py --dry-run # Preview first
|
||||||
python cloudkit_import.py --stadiums-only ... # Stadiums first
|
python cloudkit_import.py --key-id XX --key-file key.p8 # Import all
|
||||||
python cloudkit_import.py --games-only ... # Games after
|
python cloudkit_import.py --stadiums-only # Stadiums first
|
||||||
python cloudkit_import.py --stadium-aliases-only ... # Stadium aliases only
|
python cloudkit_import.py --games-only # All games
|
||||||
python cloudkit_import.py --delete-all ... # Delete then import
|
python cloudkit_import.py --games-files mlb_2025.json # Specific game file
|
||||||
python cloudkit_import.py --delete-only ... # Delete only (no import)
|
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
|
import argparse, json, time, os, sys, hashlib, base64, requests
|
||||||
@@ -48,6 +67,58 @@ DEFAULT_KEY_ID = "152be0715e0276e31aaea5cbfe79dc872f298861a55c70fae14e5fe3e026cf
|
|||||||
DEFAULT_KEY_FILE = "eckey.pem"
|
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():
|
def show_menu():
|
||||||
"""Show interactive menu and return selected action."""
|
"""Show interactive menu and return selected action."""
|
||||||
print("\n" + "="*50)
|
print("\n" + "="*50)
|
||||||
@@ -55,25 +126,26 @@ def show_menu():
|
|||||||
print("="*50)
|
print("="*50)
|
||||||
print("\n 1. Import all (stadiums, teams, games, league structure, team aliases, stadium aliases)")
|
print("\n 1. Import all (stadiums, teams, games, league structure, team aliases, stadium aliases)")
|
||||||
print(" 2. Stadiums only")
|
print(" 2. Stadiums only")
|
||||||
print(" 3. Games only")
|
print(" 3. Games only (all files)")
|
||||||
print(" 4. League structure only")
|
print(" 4. Games - select specific files")
|
||||||
print(" 5. Team aliases only")
|
print(" 5. League structure only")
|
||||||
print(" 6. Stadium aliases only")
|
print(" 6. Team aliases only")
|
||||||
print(" 7. Canonical only (league structure + team aliases + stadium aliases)")
|
print(" 7. Stadium aliases only")
|
||||||
print(" 8. Delete all then import")
|
print(" 8. Canonical only (league structure + team aliases + stadium aliases)")
|
||||||
print(" 9. Delete only (no import)")
|
print(" 9. Delete all then import")
|
||||||
print(" 10. Dry run (preview only)")
|
print(" 10. Delete only (no import)")
|
||||||
|
print(" 11. Dry run (preview only)")
|
||||||
print(" 0. Exit")
|
print(" 0. Exit")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
choice = input("Enter choice [1-10, 0 to exit]: ").strip()
|
choice = input("Enter choice [1-11, 0 to exit]: ").strip()
|
||||||
if choice == '0':
|
if choice == '0':
|
||||||
return None
|
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)
|
return int(choice)
|
||||||
print("Invalid choice. Please enter 1-10 or 0.")
|
print("Invalid choice. Please enter 1-11 or 0.")
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
print("\nExiting.")
|
print("\nExiting.")
|
||||||
return None
|
return None
|
||||||
@@ -265,6 +337,7 @@ def main():
|
|||||||
p.add_argument('--data-dir', default='./data')
|
p.add_argument('--data-dir', default='./data')
|
||||||
p.add_argument('--stadiums-only', action='store_true')
|
p.add_argument('--stadiums-only', action='store_true')
|
||||||
p.add_argument('--games-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('--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('--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')
|
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
|
# Show interactive menu if no action flags provided or --interactive
|
||||||
has_action_flag = any([
|
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.team_aliases_only, args.stadium_aliases_only, args.canonical_only,
|
||||||
args.delete_all, args.delete_only, args.dry_run
|
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:
|
if args.interactive or not has_action_flag:
|
||||||
choice = show_menu()
|
choice = show_menu()
|
||||||
if choice is None:
|
if choice is None:
|
||||||
@@ -293,21 +373,27 @@ def main():
|
|||||||
pass # Default behavior
|
pass # Default behavior
|
||||||
elif choice == 2: # Stadiums only
|
elif choice == 2: # Stadiums only
|
||||||
args.stadiums_only = True
|
args.stadiums_only = True
|
||||||
elif choice == 3: # Games only
|
elif choice == 3: # Games only (all files)
|
||||||
args.games_only = True
|
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
|
args.league_structure_only = True
|
||||||
elif choice == 5: # Team aliases only
|
elif choice == 6: # Team aliases only
|
||||||
args.team_aliases_only = True
|
args.team_aliases_only = True
|
||||||
elif choice == 6: # Stadium aliases only
|
elif choice == 7: # Stadium aliases only
|
||||||
args.stadium_aliases_only = True
|
args.stadium_aliases_only = True
|
||||||
elif choice == 7: # Canonical only
|
elif choice == 8: # Canonical only
|
||||||
args.canonical_only = True
|
args.canonical_only = True
|
||||||
elif choice == 8: # Delete all then import
|
elif choice == 9: # Delete all then import
|
||||||
args.delete_all = True
|
args.delete_all = True
|
||||||
elif choice == 9: # Delete only
|
elif choice == 10: # Delete only
|
||||||
args.delete_only = True
|
args.delete_only = True
|
||||||
elif choice == 10: # Dry run
|
elif choice == 11: # Dry run
|
||||||
args.dry_run = True
|
args.dry_run = True
|
||||||
|
|
||||||
print(f"\n{'='*50}")
|
print(f"\n{'='*50}")
|
||||||
@@ -332,12 +418,34 @@ def main():
|
|||||||
else:
|
else:
|
||||||
teams = [] # Legacy: extracted from stadiums
|
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 = json.load(open(data_dir / 'games_canonical.json'))
|
||||||
|
games_source = "games_canonical.json"
|
||||||
elif (data_dir / 'games.json').exists():
|
elif (data_dir / 'games.json').exists():
|
||||||
games = json.load(open(data_dir / 'games.json'))
|
games = json.load(open(data_dir / 'games.json'))
|
||||||
else:
|
games_source = "games.json (legacy)"
|
||||||
games = []
|
|
||||||
|
|
||||||
league_structure = json.load(open(data_dir / 'league_structure.json')) if (data_dir / 'league_structure.json').exists() else []
|
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 []
|
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"Using {'canonical' if use_canonical else 'legacy'} format")
|
||||||
print(f"Loaded {len(stadiums)} stadiums, {len(teams)} teams, {len(games)} games")
|
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")
|
print(f"Loaded {len(league_structure)} league structures, {len(team_aliases)} team aliases, {len(stadium_aliases)} stadium aliases\n")
|
||||||
|
|
||||||
ck = None
|
ck = None
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,10 @@
|
|||||||
"error_count": 0,
|
"error_count": 0,
|
||||||
"warning_count": 0,
|
"warning_count": 0,
|
||||||
"summary": {
|
"summary": {
|
||||||
"stadiums": 92,
|
"stadiums": 148,
|
||||||
"teams": 92,
|
"teams": 92,
|
||||||
"games": 4972,
|
"games": 0,
|
||||||
"aliases": 130,
|
"aliases": 194,
|
||||||
"by_category": {}
|
"by_category": {}
|
||||||
},
|
},
|
||||||
"errors": []
|
"errors": []
|
||||||
|
|||||||
+5769
-4973
File diff suppressed because it is too large
Load Diff
+12624
-684
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
+1
-59666
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -575,6 +575,390 @@
|
|||||||
"valid_from": null,
|
"valid_from": null,
|
||||||
"valid_until": 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",
|
"alias_name": "daikin park",
|
||||||
"stadium_canonical_id": "stadium_mlb_minute_maid_park",
|
"stadium_canonical_id": "stadium_mlb_minute_maid_park",
|
||||||
|
|||||||
+181
-93
@@ -1,93 +1,181 @@
|
|||||||
id,name,city,state,latitude,longitude,capacity,sport,team_abbrevs,source,year_opened
|
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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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,
|
||||||
|
|||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1286,5 +1286,789 @@
|
|||||||
"WPG"
|
"WPG"
|
||||||
],
|
],
|
||||||
"year_opened": null
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -31,9 +31,24 @@ from dataclasses import dataclass, asdict
|
|||||||
|
|
||||||
# Import pipeline components
|
# Import pipeline components
|
||||||
from scrape_schedules import (
|
from scrape_schedules import (
|
||||||
scrape_nba_basketball_reference,
|
ScraperSource, scrape_with_fallback,
|
||||||
scrape_mlb_statsapi,
|
# NBA sources
|
||||||
scrape_nhl_hockey_reference,
|
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,
|
generate_stadiums_from_teams,
|
||||||
assign_stable_ids,
|
assign_stable_ids,
|
||||||
export_to_json,
|
export_to_json,
|
||||||
@@ -114,28 +129,90 @@ def run_pipeline(
|
|||||||
all_stadiums = generate_stadiums_from_teams()
|
all_stadiums = generate_stadiums_from_teams()
|
||||||
print(f" Generated {len(all_stadiums)} stadiums from team data")
|
print(f" Generated {len(all_stadiums)} stadiums from team data")
|
||||||
|
|
||||||
# Scrape NBA
|
# Scrape all sports with multi-source fallback
|
||||||
print_section(f"NBA {season}")
|
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_season = f"{season-1}-{str(season)[2:]}"
|
||||||
nba_games = assign_stable_ids(nba_games, 'NBA', nba_season)
|
nba_games = assign_stable_ids(nba_games, 'NBA', nba_season)
|
||||||
all_games.extend(nba_games)
|
all_games.extend(nba_games)
|
||||||
print(f" Scraped {len(nba_games)} NBA games")
|
|
||||||
|
|
||||||
# Scrape MLB
|
|
||||||
print_section(f"MLB {season}")
|
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))
|
mlb_games = assign_stable_ids(mlb_games, 'MLB', str(season))
|
||||||
all_games.extend(mlb_games)
|
all_games.extend(mlb_games)
|
||||||
print(f" Scraped {len(mlb_games)} MLB games")
|
|
||||||
|
|
||||||
# Scrape NHL
|
|
||||||
print_section(f"NHL {season}")
|
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_season = f"{season-1}-{str(season)[2:]}"
|
||||||
nhl_games = assign_stable_ids(nhl_games, 'NHL', nhl_season)
|
nhl_games = assign_stable_ids(nhl_games, 'NHL', nhl_season)
|
||||||
all_games.extend(nhl_games)
|
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
|
# Export raw data
|
||||||
print_section("Exporting Raw Data")
|
print_section("Exporting Raw Data")
|
||||||
@@ -148,16 +225,36 @@ def run_pipeline(
|
|||||||
else:
|
else:
|
||||||
print_header("LOADING EXISTING RAW DATA")
|
print_header("LOADING EXISTING RAW DATA")
|
||||||
|
|
||||||
games_file = output_dir / 'games.json'
|
# Try loading from new structure first (games/*.json)
|
||||||
stadiums_file = output_dir / 'stadiums.json'
|
games_dir = output_dir / 'games'
|
||||||
|
raw_games = []
|
||||||
|
|
||||||
with open(games_file) as f:
|
if games_dir.exists() and any(games_dir.glob('*.json')):
|
||||||
raw_games = json.load(f)
|
print_section("Loading from games/ directory")
|
||||||
print(f" Loaded {len(raw_games)} raw games")
|
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:
|
print(f" Total: {len(raw_games):,} raw games")
|
||||||
raw_stadiums = json.load(f)
|
|
||||||
print(f" Loaded {len(raw_stadiums)} raw stadiums")
|
# 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
|
# STAGE 2: CANONICALIZE STADIUMS
|
||||||
@@ -242,13 +339,32 @@ def run_pipeline(
|
|||||||
for issue, count in by_issue.items():
|
for issue, count in by_issue.items():
|
||||||
print(f" - {issue}: {count}")
|
print(f" - {issue}: {count}")
|
||||||
|
|
||||||
# Export
|
# Export games to new structure: canonical/games/{sport}_{season}.json
|
||||||
games_canonical_path = output_dir / 'games_canonical.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:
|
with open(games_canonical_path, 'w') as f:
|
||||||
json.dump([asdict(g) for g in canonical_games_list], f, indent=2)
|
json.dump([asdict(g) for g in canonical_games_list], f, indent=2)
|
||||||
|
print(f" Exported combined to {games_canonical_path}")
|
||||||
print(f" Exported to {games_canonical_path}")
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# STAGE 5: VALIDATE
|
# STAGE 5: VALIDATE
|
||||||
@@ -320,7 +436,8 @@ def run_pipeline(
|
|||||||
print(f" - {output_dir / 'stadiums_canonical.json'}")
|
print(f" - {output_dir / 'stadiums_canonical.json'}")
|
||||||
print(f" - {output_dir / 'stadium_aliases.json'}")
|
print(f" - {output_dir / 'stadium_aliases.json'}")
|
||||||
print(f" - {output_dir / 'teams_canonical.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(f" - {output_dir / 'canonicalization_validation.json'}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|||||||
+112
-10
@@ -23,10 +23,24 @@ from enum import Enum
|
|||||||
|
|
||||||
# Import our modules
|
# Import our modules
|
||||||
from scrape_schedules import (
|
from scrape_schedules import (
|
||||||
Game, Stadium,
|
Game, Stadium, ScraperSource, scrape_with_fallback,
|
||||||
scrape_nba_basketball_reference,
|
# NBA sources
|
||||||
scrape_mlb_statsapi, scrape_mlb_baseball_reference,
|
scrape_nba_basketball_reference, scrape_nba_espn, scrape_nba_cbssports,
|
||||||
scrape_nhl_hockey_reference,
|
# 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,
|
generate_stadiums_from_teams,
|
||||||
export_to_json,
|
export_to_json,
|
||||||
assign_stable_ids,
|
assign_stable_ids,
|
||||||
@@ -119,10 +133,15 @@ def run_pipeline(
|
|||||||
all_stadiums = generate_stadiums_from_teams()
|
all_stadiums = generate_stadiums_from_teams()
|
||||||
print(f" Generated {len(all_stadiums)} stadiums from team data")
|
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']:
|
if sport in ['nba', 'all']:
|
||||||
print_section(f"NBA {season}")
|
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_season = f"{season-1}-{str(season)[2:]}"
|
||||||
nba_games = assign_stable_ids(nba_games, 'NBA', nba_season)
|
nba_games = assign_stable_ids(nba_games, 'NBA', nba_season)
|
||||||
all_games.extend(nba_games)
|
all_games.extend(nba_games)
|
||||||
@@ -130,19 +149,91 @@ def run_pipeline(
|
|||||||
|
|
||||||
if sport in ['mlb', 'all']:
|
if sport in ['mlb', 'all']:
|
||||||
print_section(f"MLB {season}")
|
print_section(f"MLB {season}")
|
||||||
mlb_games = scrape_mlb_statsapi(season)
|
mlb_sources = [
|
||||||
# MLB API uses official gamePk - already stable
|
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)
|
all_games.extend(mlb_games)
|
||||||
games_by_sport['MLB'] = len(mlb_games)
|
games_by_sport['MLB'] = len(mlb_games)
|
||||||
|
|
||||||
if sport in ['nhl', 'all']:
|
if sport in ['nhl', 'all']:
|
||||||
print_section(f"NHL {season}")
|
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_season = f"{season-1}-{str(season)[2:]}"
|
||||||
nhl_games = assign_stable_ids(nhl_games, 'NHL', nhl_season)
|
nhl_games = assign_stable_ids(nhl_games, 'NHL', nhl_season)
|
||||||
all_games.extend(nhl_games)
|
all_games.extend(nhl_games)
|
||||||
games_by_sport['NHL'] = len(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
|
# Export data
|
||||||
print_section("Exporting Data")
|
print_section("Exporting Data")
|
||||||
export_to_json(all_games, all_stadiums, output_dir)
|
export_to_json(all_games, all_stadiums, output_dir)
|
||||||
@@ -233,6 +324,17 @@ def run_pipeline(
|
|||||||
if count < 75 or count > 90:
|
if count < 75 or count > 90:
|
||||||
print(f" NHL: {team} has {count} games (expected ~82)")
|
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
|
# PHASE 3: GENERATE REPORT
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -396,7 +498,7 @@ Examples:
|
|||||||
help='Season year (default: 2025)'
|
help='Season year (default: 2025)'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
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)'
|
help='Sport to process (default: all)'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
+2407
-24
File diff suppressed because it is too large
Load Diff
Executable
+1092
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,32 @@ EXPECTED_GAMES = {
|
|||||||
'max': 168,
|
'max': 168,
|
||||||
'description': 'MLB regular season (162 games)'
|
'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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ from scrape_schedules import (
|
|||||||
scrape_nba_basketball_reference,
|
scrape_nba_basketball_reference,
|
||||||
scrape_mlb_statsapi, scrape_mlb_baseball_reference,
|
scrape_mlb_statsapi, scrape_mlb_baseball_reference,
|
||||||
scrape_nhl_hockey_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,
|
assign_stable_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -136,7 +139,11 @@ def generate_game_key(game: Game) -> str:
|
|||||||
|
|
||||||
def normalize_team_name(name: str, sport: str) -> str:
|
def normalize_team_name(name: str, sport: str) -> str:
|
||||||
"""Normalize team name variations."""
|
"""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()
|
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('--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('--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('--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')
|
parser.add_argument('--output', type=str, default='./data/validation_report.json')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
|
|||||||
case nhl = "NHL"
|
case nhl = "NHL"
|
||||||
case nfl = "NFL"
|
case nfl = "NFL"
|
||||||
case mls = "MLS"
|
case mls = "MLS"
|
||||||
|
case wnba = "WNBA"
|
||||||
|
case nwsl = "NWSL"
|
||||||
|
case cbb = "CBB"
|
||||||
|
|
||||||
var id: String { rawValue }
|
var id: String { rawValue }
|
||||||
|
|
||||||
@@ -22,6 +25,9 @@ enum Sport: String, Codable, CaseIterable, Identifiable {
|
|||||||
case .nhl: return "National Hockey League"
|
case .nhl: return "National Hockey League"
|
||||||
case .nfl: return "National Football League"
|
case .nfl: return "National Football League"
|
||||||
case .mls: return "Major League Soccer"
|
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 .nhl: return "hockey.puck.fill"
|
||||||
case .nfl: return "football.fill"
|
case .nfl: return "football.fill"
|
||||||
case .mls: return "soccerball"
|
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 .nhl: return .blue
|
||||||
case .nfl: return .brown
|
case .nfl: return .brown
|
||||||
case .mls: return .green
|
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 .nhl: return (10, 6) // October - June (wraps)
|
||||||
case .nfl: return (9, 2) // September - February (wraps)
|
case .nfl: return (9, 2) // September - February (wraps)
|
||||||
case .mls: return (2, 12) // February - December
|
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] {
|
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)])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ enum PlanningMode: String, Codable, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
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 .gameFirst: return "Build trip around specific games"
|
||||||
case .locations: return "Plan route between locations"
|
case .locations: return "Plan route between locations"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ enum Theme {
|
|||||||
static let nhlBlue = Color(hex: "003087")
|
static let nhlBlue = Color(hex: "003087")
|
||||||
static let nflBrown = Color(hex: "8B5A2B")
|
static let nflBrown = Color(hex: "8B5A2B")
|
||||||
static let mlsGreen = Color(hex: "00A651")
|
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
|
// MARK: - Dark Mode Colors
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,9 @@ extension Sport {
|
|||||||
case .nhl: return Theme.nhlBlue
|
case .nhl: return Theme.nhlBlue
|
||||||
case .nfl: return Theme.nflBrown
|
case .nfl: return Theme.nflBrown
|
||||||
case .mls: return Theme.mlsGreen
|
case .mls: return Theme.mlsGreen
|
||||||
|
case .wnba: return Theme.wnbaPurple
|
||||||
|
case .nwsl: return Theme.nwslTeal
|
||||||
|
case .cbb: return Theme.cbbMint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,19 +169,40 @@ struct HomeView: View {
|
|||||||
// MARK: - Quick Actions
|
// MARK: - Quick Actions
|
||||||
|
|
||||||
private var quickActions: some View {
|
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")
|
Text("Quick Start")
|
||||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||||
|
|
||||||
HStack(spacing: Theme.Spacing.sm) {
|
VStack(spacing: Theme.Spacing.md) {
|
||||||
ForEach(Sport.supported) { sport in
|
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
|
||||||
QuickSportButton(sport: sport) {
|
HStack(spacing: Theme.Spacing.sm) {
|
||||||
selectedSport = sport
|
ForEach(row) { sport in
|
||||||
showNewTrip = true
|
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 {
|
var body: some View {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
VStack(spacing: Theme.Spacing.xs) {
|
VStack(spacing: 6) {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(sport.themeColor.opacity(0.15))
|
.fill(sport.themeColor.opacity(0.15))
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 48, height: 48)
|
||||||
|
|
||||||
Image(systemName: sport.iconName)
|
Image(systemName: sport.iconName)
|
||||||
.font(.title2)
|
.font(.system(size: 20))
|
||||||
.foregroundStyle(sport.themeColor)
|
.foregroundStyle(sport.themeColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(sport.rawValue)
|
Text(sport.rawValue)
|
||||||
.font(.system(size: Theme.FontSize.micro, weight: .medium))
|
.font(.system(size: 10, weight: .medium))
|
||||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, Theme.Spacing.sm)
|
.scaleEffect(isPressed ? 0.9 : 1.0)
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.simultaneousGesture(
|
.simultaneousGesture(
|
||||||
|
|||||||
@@ -244,13 +244,7 @@ struct SettingsView: View {
|
|||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private func sportColor(for sport: Sport) -> Color {
|
private func sportColor(for sport: Sport) -> Color {
|
||||||
switch sport {
|
sport.themeColor
|
||||||
case .mlb: return .red
|
|
||||||
case .nba: return .orange
|
|
||||||
case .nhl: return .blue
|
|
||||||
case .nfl: return .green
|
|
||||||
case .mls: return .purple
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,24 @@ final class TripCreationViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
var startDate: Date = Date()
|
var startDate: Date = Date() {
|
||||||
var endDate: Date = Date().addingTimeInterval(86400 * 7)
|
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)
|
// Trip duration for game-first mode (days before/after selected games)
|
||||||
var tripBufferDays: Int = 2
|
var tripBufferDays: Int = 2
|
||||||
|
|||||||
@@ -524,22 +524,36 @@ struct TripCreationView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var sportsSection: some View {
|
private var sportsSection: some View {
|
||||||
ThemedSection(title: "Sports") {
|
let sports = Sport.supported
|
||||||
HStack(spacing: Theme.Spacing.sm) {
|
let rows = sports.chunked(into: 4)
|
||||||
ForEach(Sport.supported) { sport in
|
|
||||||
SportSelectionChip(
|
return ThemedSection(title: "Sports") {
|
||||||
sport: sport,
|
VStack(spacing: Theme.Spacing.sm) {
|
||||||
isSelected: viewModel.selectedSports.contains(sport),
|
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
|
||||||
onTap: {
|
HStack(spacing: Theme.Spacing.sm) {
|
||||||
if viewModel.selectedSports.contains(sport) {
|
ForEach(row) { sport in
|
||||||
viewModel.selectedSports.remove(sport)
|
SportSelectionChip(
|
||||||
} else {
|
sport: sport,
|
||||||
viewModel.selectedSports.insert(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 isSelected: Bool
|
||||||
let onTap: () -> Void
|
let onTap: () -> Void
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@State private var isPressed = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: onTap) {
|
Button(action: onTap) {
|
||||||
VStack(spacing: Theme.Spacing.xs) {
|
VStack(spacing: 6) {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(isSelected ? sport.themeColor : sport.themeColor.opacity(0.15))
|
.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)
|
Image(systemName: sport.iconName)
|
||||||
.font(.title3)
|
.font(.system(size: 20))
|
||||||
.foregroundStyle(isSelected ? .white : sport.themeColor)
|
.foregroundStyle(isSelected ? .white : sport.themeColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(sport.rawValue)
|
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))
|
.foregroundStyle(isSelected ? Theme.textPrimary(colorScheme) : Theme.textSecondary(colorScheme))
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.scaleEffect(isPressed ? 0.9 : 1.0)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.simultaneousGesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { _ in
|
||||||
|
withAnimation(Theme.Animation.spring) { isPressed = true }
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
withAnimation(Theme.Animation.spring) { isPressed = false }
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// GameDAGRouter.swift
|
// GameDAGRouter.swift
|
||||||
// SportsTime
|
// 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?"
|
// 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?"
|
// This IS: "what time-respecting paths exist through a graph of games?"
|
||||||
@@ -10,11 +10,14 @@
|
|||||||
// The algorithm:
|
// The algorithm:
|
||||||
// 1. Bucket games by calendar day
|
// 1. Bucket games by calendar day
|
||||||
// 2. Build directed edges where time moves forward AND driving is feasible
|
// 2. Build directed edges where time moves forward AND driving is feasible
|
||||||
// 3. Beam search: keep top K paths at each depth
|
// 3. Generate routes via beam search
|
||||||
// 4. Dominance pruning: discard inferior paths
|
// 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
|
// The diversity system ensures users see:
|
||||||
// (vs 2^78 for naive subset enumeration)
|
// - 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
|
import Foundation
|
||||||
@@ -24,12 +27,11 @@ enum GameDAGRouter {
|
|||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
|
|
||||||
/// Default beam width - how many partial routes to keep at each step
|
/// Default beam width during expansion
|
||||||
/// Increased to ensure we preserve diverse route lengths (short and long trips)
|
private static let defaultBeamWidth = 100
|
||||||
private static let defaultBeamWidth = 50
|
|
||||||
|
|
||||||
/// Maximum options to return (increased to provide more diverse trip lengths)
|
/// Maximum options to return (diverse sample)
|
||||||
private static let maxOptions = 50
|
private static let maxOptions = 75
|
||||||
|
|
||||||
/// Buffer time after game ends before we can depart (hours)
|
/// Buffer time after game ends before we can depart (hours)
|
||||||
private static let gameEndBufferHours: Double = 3.0
|
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)
|
/// Maximum days ahead to consider for next game (1 = next day only, 5 = allows multi-day drives)
|
||||||
private static let maxDayLookahead = 5
|
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
|
// 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:
|
/// - 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
|
/// - stadiums: Dictionary mapping stadium IDs to Stadium objects
|
||||||
/// - constraints: Driving constraints (number of drivers, max hours per day)
|
/// - constraints: Driving constraints (max hours per day)
|
||||||
/// - anchorGameIds: Games that MUST appear in every valid route (for Scenario B)
|
/// - anchorGameIds: Games that MUST appear in every valid route
|
||||||
/// - allowRepeatCities: If false, each city can only appear once in a 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(
|
static func findRoutes(
|
||||||
games: [Game],
|
games: [Game],
|
||||||
@@ -65,21 +101,18 @@ enum GameDAGRouter {
|
|||||||
// Edge cases
|
// Edge cases
|
||||||
guard !games.isEmpty else { return [] }
|
guard !games.isEmpty else { return [] }
|
||||||
if games.count == 1 {
|
if games.count == 1 {
|
||||||
// Single game - just return it if it satisfies anchors
|
|
||||||
if anchorGameIds.isEmpty || anchorGameIds.contains(games[0].id) {
|
if anchorGameIds.isEmpty || anchorGameIds.contains(games[0].id) {
|
||||||
return [games]
|
return [games]
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if games.count == 2 {
|
if games.count == 2 {
|
||||||
// Two games - check if both are reachable
|
|
||||||
let sorted = games.sorted { $0.startTime < $1.startTime }
|
let sorted = games.sorted { $0.startTime < $1.startTime }
|
||||||
if canTransition(from: sorted[0], to: sorted[1], stadiums: stadiums, constraints: constraints) {
|
if canTransition(from: sorted[0], to: sorted[1], stadiums: stadiums, constraints: constraints) {
|
||||||
if anchorGameIds.isSubset(of: Set(sorted.map { $0.id })) {
|
if anchorGameIds.isSubset(of: Set(sorted.map { $0.id })) {
|
||||||
return [sorted]
|
return [sorted]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Can't connect them - return individual games if they satisfy anchors
|
|
||||||
if anchorGameIds.isEmpty {
|
if anchorGameIds.isEmpty {
|
||||||
return [[sorted[0]], [sorted[1]]]
|
return [[sorted[0]], [sorted[1]]]
|
||||||
}
|
}
|
||||||
@@ -95,18 +128,9 @@ enum GameDAGRouter {
|
|||||||
|
|
||||||
guard !sortedDays.isEmpty else { return [] }
|
guard !sortedDays.isEmpty else { return [] }
|
||||||
|
|
||||||
|
// Step 3: Initialize beam with first few days' games as starting points
|
||||||
// Step 3: Initialize beam with first day's games
|
|
||||||
var beam: [[Game]] = []
|
var beam: [[Game]] = []
|
||||||
if let firstDayGames = buckets[sortedDays[0]] {
|
for dayIndex in sortedDays.prefix(maxDayLookahead) {
|
||||||
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) {
|
|
||||||
if let dayGames = buckets[dayIndex] {
|
if let dayGames = buckets[dayIndex] {
|
||||||
for game in dayGames {
|
for game in dayGames {
|
||||||
beam.append([game])
|
beam.append([game])
|
||||||
@@ -114,9 +138,8 @@ enum GameDAGRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Step 4: Expand beam day by day
|
// Step 4: Expand beam day by day
|
||||||
for (_, dayIndex) in sortedDays.dropFirst().enumerated() {
|
for dayIndex in sortedDays.dropFirst() {
|
||||||
let todaysGames = buckets[dayIndex] ?? []
|
let todaysGames = buckets[dayIndex] ?? []
|
||||||
var nextBeam: [[Game]] = []
|
var nextBeam: [[Game]] = []
|
||||||
|
|
||||||
@@ -124,36 +147,34 @@ enum GameDAGRouter {
|
|||||||
guard let lastGame = path.last else { continue }
|
guard let lastGame = path.last else { continue }
|
||||||
let lastGameDay = dayIndexFor(lastGame.startTime, referenceDate: sortedGames[0].startTime)
|
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 {
|
if dayIndex > lastGameDay + maxDayLookahead {
|
||||||
// This path is too far behind, keep it as-is
|
|
||||||
nextBeam.append(path)
|
nextBeam.append(path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try adding each of today's games
|
// Try adding each of today's games
|
||||||
for candidate in todaysGames {
|
for candidate in todaysGames {
|
||||||
// Check for repeat city violation during route building
|
// Check for repeat city violation
|
||||||
if !allowRepeatCities {
|
if !allowRepeatCities {
|
||||||
let candidateCity = stadiums[candidate.stadiumId]?.city ?? ""
|
let candidateCity = stadiums[candidate.stadiumId]?.city ?? ""
|
||||||
let pathCities = Set(path.compactMap { stadiums[$0.stadiumId]?.city })
|
let pathCities = Set(path.compactMap { stadiums[$0.stadiumId]?.city })
|
||||||
if pathCities.contains(candidateCity) {
|
if pathCities.contains(candidateCity) {
|
||||||
continue // Skip - would violate allowRepeatCities
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if canTransition(from: lastGame, to: candidate, stadiums: stadiums, constraints: constraints) {
|
if canTransition(from: lastGame, to: candidate, stadiums: stadiums, constraints: constraints) {
|
||||||
let newPath = path + [candidate]
|
nextBeam.append(path + [candidate])
|
||||||
nextBeam.append(newPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also keep the path without adding a game today (allows off-days)
|
// Keep the path without adding a game today
|
||||||
nextBeam.append(path)
|
nextBeam.append(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dominance pruning + beam truncation
|
// Diversity-aware pruning during expansion
|
||||||
beam = pruneAndTruncate(nextBeam, beamWidth: beamWidth, stadiums: stadiums)
|
beam = diversityPrune(nextBeam, stadiums: stadiums, targetCount: beamWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Filter routes that contain all anchors
|
// Step 5: Filter routes that contain all anchors
|
||||||
@@ -162,21 +183,15 @@ enum GameDAGRouter {
|
|||||||
return anchorGameIds.isSubset(of: pathGameIds)
|
return anchorGameIds.isSubset(of: pathGameIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Ensure geographic diversity in results
|
// Step 6: Final diversity selection
|
||||||
// Group routes by their primary region (city with most games)
|
|
||||||
// Then pick the best route from each region
|
|
||||||
let finalRoutes = selectDiverseRoutes(routesWithAnchors, stadiums: stadiums, maxCount: maxOptions)
|
let finalRoutes = selectDiverseRoutes(routesWithAnchors, stadiums: stadiums, maxCount: maxOptions)
|
||||||
|
|
||||||
print("🔍 DAG: Input games=\(games.count), beam final=\(beam.count), withAnchors=\(routesWithAnchors.count), final=\(finalRoutes.count)")
|
print("🔍 DAG: Input games=\(games.count), beam=\(beam.count), withAnchors=\(routesWithAnchors.count), final=\(finalRoutes.count)")
|
||||||
if let best = finalRoutes.first {
|
|
||||||
print("🔍 DAG: Best route has \(best.count) games")
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalRoutes
|
return finalRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compatibility wrapper that matches GeographicRouteExplorer's interface.
|
/// Compatibility wrapper that matches GeographicRouteExplorer's interface.
|
||||||
/// This allows drop-in replacement in ScenarioAPlanner and ScenarioBPlanner.
|
|
||||||
static func findAllSensibleRoutes(
|
static func findAllSensibleRoutes(
|
||||||
from games: [Game],
|
from games: [Game],
|
||||||
stadiums: [UUID: Stadium],
|
stadiums: [UUID: Stadium],
|
||||||
@@ -184,9 +199,7 @@ enum GameDAGRouter {
|
|||||||
allowRepeatCities: Bool = true,
|
allowRepeatCities: Bool = true,
|
||||||
stopBuilder: ([Game], [UUID: Stadium]) -> [ItineraryStop]
|
stopBuilder: ([Game], [UUID: Stadium]) -> [ItineraryStop]
|
||||||
) -> [[Game]] {
|
) -> [[Game]] {
|
||||||
// Use default driving constraints
|
|
||||||
let constraints = DrivingConstraints.default
|
let constraints = DrivingConstraints.default
|
||||||
|
|
||||||
return findRoutes(
|
return findRoutes(
|
||||||
games: games,
|
games: games,
|
||||||
stadiums: stadiums,
|
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
|
// 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]] {
|
private static func bucketByDay(games: [Game]) -> [Int: [Game]] {
|
||||||
guard let firstGame = games.first else { return [:] }
|
guard let firstGame = games.first else { return [:] }
|
||||||
let referenceDate = firstGame.startTime
|
let referenceDate = firstGame.startTime
|
||||||
@@ -211,24 +451,15 @@ enum GameDAGRouter {
|
|||||||
return buckets
|
return buckets
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the day index for a date relative to a reference date.
|
|
||||||
private static func dayIndexFor(_ date: Date, referenceDate: Date) -> Int {
|
private static func dayIndexFor(_ date: Date, referenceDate: Date) -> Int {
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let refDay = calendar.startOfDay(for: referenceDate)
|
let refDay = calendar.startOfDay(for: referenceDate)
|
||||||
let dateDay = calendar.startOfDay(for: date)
|
let dateDay = calendar.startOfDay(for: date)
|
||||||
let components = calendar.dateComponents([.day], from: refDay, to: dateDay)
|
return calendar.dateComponents([.day], from: refDay, to: dateDay).day ?? 0
|
||||||
return components.day ?? 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Transition Feasibility
|
// 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(
|
private static func canTransition(
|
||||||
from: Game,
|
from: Game,
|
||||||
to: Game,
|
to: Game,
|
||||||
@@ -236,289 +467,62 @@ enum GameDAGRouter {
|
|||||||
constraints: DrivingConstraints
|
constraints: DrivingConstraints
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
// Time must move forward
|
// Time must move forward
|
||||||
guard to.startTime > from.startTime else {
|
guard to.startTime > from.startTime else { return false }
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same stadium = always feasible (no driving needed)
|
// Same stadium = always feasible
|
||||||
if from.stadiumId == to.stadiumId { return true }
|
if from.stadiumId == to.stadiumId { return true }
|
||||||
|
|
||||||
// Get stadiums
|
// Get stadiums
|
||||||
guard let fromStadium = stadiums[from.stadiumId],
|
guard let fromStadium = stadiums[from.stadiumId],
|
||||||
let toStadium = stadiums[to.stadiumId] else {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let fromCoord = fromStadium.coordinate
|
|
||||||
let toCoord = toStadium.coordinate
|
|
||||||
|
|
||||||
// Calculate driving time
|
// Calculate driving time
|
||||||
let distanceMiles = TravelEstimator.haversineDistanceMiles(
|
let distanceMiles = TravelEstimator.haversineDistanceMiles(
|
||||||
from: CLLocationCoordinate2D(latitude: fromCoord.latitude, longitude: fromCoord.longitude),
|
from: CLLocationCoordinate2D(latitude: fromStadium.coordinate.latitude, longitude: fromStadium.coordinate.longitude),
|
||||||
to: CLLocationCoordinate2D(latitude: toCoord.latitude, longitude: toCoord.longitude)
|
to: CLLocationCoordinate2D(latitude: toStadium.coordinate.latitude, longitude: toStadium.coordinate.longitude)
|
||||||
) * 1.3 // Road routing factor
|
) * 1.3 // Road routing factor
|
||||||
|
|
||||||
let drivingHours = distanceMiles / 60.0 // Average 60 mph
|
let drivingHours = distanceMiles / 60.0
|
||||||
|
|
||||||
// Calculate available driving time between games
|
// Calculate available time
|
||||||
// After game A ends (+ buffer), how much time until game B starts (- buffer)?
|
|
||||||
let departureTime = from.startTime.addingTimeInterval(gameEndBufferHours * 3600)
|
let departureTime = from.startTime.addingTimeInterval(gameEndBufferHours * 3600)
|
||||||
let deadline = to.startTime.addingTimeInterval(-3600) // 1 hour buffer before game
|
let deadline = to.startTime.addingTimeInterval(-3600) // 1 hour buffer before game
|
||||||
let availableSeconds = deadline.timeIntervalSince(departureTime)
|
let availableHours = deadline.timeIntervalSince(departureTime) / 3600.0
|
||||||
let availableHours = availableSeconds / 3600.0
|
|
||||||
|
|
||||||
// Calculate how many driving days we have
|
// Calculate driving days available
|
||||||
// Each day can have maxDailyDrivingHours of driving
|
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let fromDay = calendar.startOfDay(for: from.startTime)
|
let daysBetween = calendar.dateComponents(
|
||||||
let toDay = calendar.startOfDay(for: to.startTime)
|
[.day],
|
||||||
let daysBetween = calendar.dateComponents([.day], from: fromDay, to: toDay).day ?? 0
|
from: calendar.startOfDay(for: from.startTime),
|
||||||
|
to: calendar.startOfDay(for: to.startTime)
|
||||||
|
).day ?? 0
|
||||||
|
|
||||||
// Available driving hours = days between * max per day
|
let maxDrivingHoursAvailable = daysBetween == 0
|
||||||
// (If games are same day, daysBetween = 0, but we might still have hours available)
|
? max(0, availableHours)
|
||||||
let maxDrivingHoursAvailable: Double
|
: Double(daysBetween) * constraints.maxDailyDrivingHours
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have enough driving time
|
return drivingHours <= maxDrivingHoursAvailable && drivingHours <= availableHours
|
||||||
guard drivingHours <= maxDrivingHoursAvailable else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also verify we can arrive before game starts (sanity check)
|
|
||||||
guard availableHours >= drivingHours else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Geographic Diversity
|
// MARK: - Distance Estimation
|
||||||
|
|
||||||
/// Selects diverse routes from the candidate set.
|
private static func estimateDistanceMiles(
|
||||||
/// 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(
|
|
||||||
from: Game,
|
from: Game,
|
||||||
to: Game,
|
to: Game,
|
||||||
stadiums: [UUID: Stadium]
|
stadiums: [UUID: Stadium]
|
||||||
) -> Double {
|
) -> Double {
|
||||||
// Same stadium = 0 driving
|
|
||||||
if from.stadiumId == to.stadiumId { return 0 }
|
if from.stadiumId == to.stadiumId { return 0 }
|
||||||
|
|
||||||
guard let fromStadium = stadiums[from.stadiumId],
|
guard let fromStadium = stadiums[from.stadiumId],
|
||||||
let toStadium = stadiums[to.stadiumId] else {
|
let toStadium = stadiums[to.stadiumId] else {
|
||||||
return 5.0 // Fallback: assume 5 hours
|
return 300 // Fallback estimate
|
||||||
}
|
}
|
||||||
|
|
||||||
let fromCoord = fromStadium.coordinate
|
return TravelEstimator.haversineDistanceMiles(
|
||||||
let toCoord = toStadium.coordinate
|
from: CLLocationCoordinate2D(latitude: fromStadium.coordinate.latitude, longitude: fromStadium.coordinate.longitude),
|
||||||
|
to: CLLocationCoordinate2D(latitude: toStadium.coordinate.latitude, longitude: toStadium.coordinate.longitude)
|
||||||
let distanceMiles = TravelEstimator.haversineDistanceMiles(
|
|
||||||
from: CLLocationCoordinate2D(latitude: fromCoord.latitude, longitude: fromCoord.longitude),
|
|
||||||
to: CLLocationCoordinate2D(latitude: toCoord.latitude, longitude: toCoord.longitude)
|
|
||||||
) * 1.3
|
) * 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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1286,5 +1286,789 @@
|
|||||||
"WPG"
|
"WPG"
|
||||||
],
|
],
|
||||||
"year_opened": null
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1102,5 +1102,677 @@
|
|||||||
"division_id": "nhl_central",
|
"division_id": "nhl_central",
|
||||||
"primary_color": null,
|
"primary_color": null,
|
||||||
"secondary_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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -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
Reference in New Issue
Block a user