# How to Add a New Sport to SportsTime > A guide so simple, even a 5-year-old can follow along! --- ## The Big Picture (What's Happening?) Imagine you have a **magic toy box** (that's CloudKit - Apple's cloud storage). When you put a new toy in the box, all your friends' toy boxes get the same toy automatically! That's how SportsTime works: 1. You put a new sport in the **magic cloud box** 2. Everyone's app gets the new sport - without downloading a new app! --- ## Step 1: Open the Magic Box (CloudKit Dashboard) 1. Go to [iCloud Dashboard](https://icloud.developer.apple.com/) 2. Sign in with the developer Apple ID 3. Click on **CloudKit Database** 4. Select **SportsTime** container: `iCloud.com.sportstime.app` 5. Click **Public Database** (this is the shared toy box everyone can see) ``` Think of it like this: [Your Computer] | v [CloudKit Dashboard] <-- This is where you add new sports! | v [Magic Cloud Box] | _____|_____ | | | v v v [App] [App] [App] <-- Everyone's app gets the update! ``` --- ## Step 2: Create the New Sport Record Click **Records** in the sidebar, then click the **+** button to create a new record. ### Choose the Record Type Select: `Sport` ### Fill in These Fields | Field Name | What It Means | Example | |------------|---------------|---------| | `sportId` | A short code name (no spaces!) | `mls` | | `abbreviation` | How we shorten it | `MLS` | | `displayName` | The pretty name people see | `Major League Soccer` | | `iconName` | SF Symbol name for the icon | `soccerball` | | `colorHex` | The sport's color (like a crayon!) | `#00A859` | | `seasonStartMonth` | When games start (1-12) | `3` (March) | | `seasonEndMonth` | When games end (1-12) | `11` (November) | | `isActive` | Is it turned on? (1=yes, 0=no) | `1` | | `schemaVersion` | Always use this number | `1` | | `lastModified` | Today's date | (auto-filled) | ### Example: Adding Soccer (MLS) ``` sportId: mls abbreviation: MLS displayName: Major League Soccer iconName: soccerball colorHex: #00A859 seasonStartMonth: 3 seasonEndMonth: 11 isActive: 1 schemaVersion: 1 ``` Click **Save Record**! --- ## Step 3: What Happens Next? (The Magic!) Now let's follow the journey of your new sport, step by step: ``` YOU ADD SPORT HERE | v +------------------------------------------+ | CloudKit (Magic Cloud Box) | | | | +----------------------------------+ | | | Sport Record: "MLS" | | | | - displayName: Major League... | | | | - iconName: soccerball | | | +----------------------------------+ | +------------------------------------------+ | | (App asks: "Any new sports?") v +------------------------------------------+ | User's iPhone | | | | 1. CloudKitService.fetchSportsForSync() | | "Hey cloud, what's new?" | | | | | v | | 2. CKSport.toCanonical() | | "Let me understand this..." | | | | | v | | 3. CanonicalSyncService.syncSports() | | "Save it to my phone!" | | | | | v | | 4. SwiftData (Phone's Memory) | | [MLS is now saved locally!] | | | | | v | | 5. AppDataProvider.loadInitialData() | | "Load it so the app can use it!" | | | | | v | | 6. App Shows MLS! | | "Pick your sports: MLB, NBA, MLS"| +------------------------------------------+ ``` --- ## The Journey in Kid Terms ### 1. You Put a Toy in the Cloud Box ``` You: "Here's a new soccer ball toy!" *puts MLS in CloudKit* ``` ### 2. The App Wakes Up and Checks ``` App: "Good morning! Let me check if there are new toys..." *calls CloudKitService.fetchSportsForSync()* ``` ### 3. The App Finds Your New Toy ``` Cloud: "Yes! Someone added a soccer ball!" App: "Ooh! Let me grab it!" *CKSport wraps the cloud data* ``` ### 4. The App Puts It in Its Pocket ``` App: "I'll save this soccer ball in my pocket so I don't forget it!" *CanonicalSyncService saves to SwiftData* ``` ### 5. The App Shows It to the User ``` App: "Hey user! Look what I found! You can now pick Soccer too!" *AppDataProvider.allSports includes MLS* ``` --- ## The Code Path (For Grown-Ups) Here's exactly which files do what: ``` CloudKit Dashboard | | (1) You create Sport record v CloudKitService.swift | | (2) fetchSportsForSync() downloads new sports v CKModels.swift (CKSport) | | (3) toCanonical() converts cloud data to app data v CanonicalSyncService.swift | | (4) syncSports() + mergeSport() saves to phone v CanonicalModels.swift (CanonicalSport) | | (5) Stored in SwiftData (phone's database) v DataProvider.swift | | (6) loadInitialData() loads into memory | (7) allSports returns Sport enum + DynamicSports v App UI Shows New Sport! ``` --- ## When Does the App Check for New Sports? The app checks for new toys (sports) at these times: | When | What Happens | |------|--------------| | App opens | "Let me see if anything is new!" | | Pull to refresh | "User wants me to check again!" | | Background refresh | "Checking while you're not looking..." | --- ## Troubleshooting: My Sport Isn't Showing Up! ### Checklist (Like checking your backpack!) 1. **Is `isActive` set to `1`?** - If it's `0`, the app ignores it (like a toy that's turned off) 2. **Is the `sportId` unique?** - Can't have two toys with the same name! 3. **Did you save the record?** - Click that Save button! 4. **Is the app connected to internet?** - No wifi = no checking the cloud box 5. **Try force-refreshing:** - Pull down on the screen to refresh --- ## Quick Reference Card ``` +--------------------------------------------------+ | ADDING A NEW SPORT CHECKLIST | +--------------------------------------------------+ | | | [ ] 1. Open CloudKit Dashboard | | [ ] 2. Go to Public Database | | [ ] 3. Create new "Sport" record | | [ ] 4. Fill in ALL required fields: | | - sportId (unique!) | | - abbreviation | | - displayName | | - iconName (SF Symbol) | | - colorHex | | - seasonStartMonth | | - seasonEndMonth | | - isActive = 1 | | - schemaVersion = 1 | | [ ] 5. Click Save | | [ ] 6. Open app and pull to refresh | | [ ] 7. See your new sport! | | | +--------------------------------------------------+ ``` --- ## Summary: The Whole Story 1. **You** add a sport to CloudKit (the magic cloud box) 2. **CloudKitService** fetches it when the app checks 3. **CKSport** translates cloud-speak to app-speak 4. **CanonicalSyncService** saves it to the phone 5. **AppDataProvider** serves it up to the app 6. **User** sees the new sport and smiles! **The End!** --- ## Part 2: Getting Game Data (The Data Pipeline) Now that you've added the sport definition to CloudKit, you need game data! This is where the **data scrapers** come in - they're like robots that go around collecting game schedules from websites and putting them in the cloud. --- ## Step 4: Tell the Scraper About Your New Sport ### 4a. Add to the Sports List Open `Scripts/sportstime_parser/config.py` and add your sport to the `SUPPORTED_SPORTS` list: ```python # Supported sports SUPPORTED_SPORTS: list[str] = [ "nba", "mlb", "nfl", "nhl", "mls", "wnba", "nwsl", "your_new_sport", # <-- Add your sport here! ] ``` Also add an expected game count (this helps validate that scraping worked): ```python EXPECTED_GAME_COUNTS: dict[str, int] = { "nba": 1230, "mlb": 2430, # ... other sports ... "your_new_sport": 500, # <-- Approximate games per season } ``` ### 4b. Create a Scraper Class Create a new file: `Scripts/sportstime_parser/scrapers/your_sport.py` Here's the template (like a recipe card for your robot): ```python """Your Sport scraper implementation.""" from datetime import datetime, date from typing import Optional from .base import BaseScraper, RawGameData, ScrapeResult from ..models.game import Game from ..models.team import Team from ..models.stadium import Stadium class YourSportScraper(BaseScraper): """Your Sport schedule scraper. Sources (in priority order): 1. ESPN API - Primary source 2. Official League Website - Backup """ def __init__(self, season: int, **kwargs): super().__init__("your_sport", season, **kwargs) def _get_sources(self) -> list[str]: """Return source list in priority order.""" return ["espn", "official_site"] def _get_source_url(self, source: str, **kwargs) -> str: """Build URL for a source.""" if source == "espn": # ESPN API URL for your sport return f"https://site.api.espn.com/apis/site/v2/sports/..." raise ValueError(f"Unknown source: {source}") def _get_season_months(self) -> list[tuple[int, int]]: """What months does the season run?""" # Return list of (year, month) tuples return [(self.season, month) for month in range(3, 12)] def _scrape_games_from_source(self, source: str) -> list[RawGameData]: """Scrape games from a specific source.""" if source == "espn": return self._scrape_espn() raise ValueError(f"Unknown source: {source}") def _scrape_espn(self) -> list[RawGameData]: """Scrape games from ESPN API.""" # Your scraping logic here pass def _normalize_games( self, raw_games: list[RawGameData], ) -> tuple[list[Game], list]: """Convert raw data to Game objects.""" # Your normalization logic here pass def scrape_teams(self) -> list[Team]: """Get all teams for your sport.""" pass def scrape_stadiums(self) -> list[Stadium]: """Get all stadiums for your sport.""" pass def create_your_sport_scraper(season: int) -> YourSportScraper: """Factory function to create the scraper.""" return YourSportScraper(season=season) ``` ### 4c. Register the Scraper Open `Scripts/sportstime_parser/cli.py` and add your scraper to the `get_scraper()` function: ```python def get_scraper(sport: str, season: int): if sport == "nba": from .scrapers.nba import create_nba_scraper return create_nba_scraper(season) # ... other sports ... elif sport == "your_sport": # <-- Add this! from .scrapers.your_sport import create_your_sport_scraper return create_your_sport_scraper(season) else: raise NotImplementedError(f"Scraper for {sport} not yet implemented") ``` --- ## Step 5: Run the Scraper Robot! Now let's send your robot out to collect games: ```bash # Go to the Scripts folder cd Scripts # Install dependencies (only needed once) pip install -r requirements.txt # Scrape your sport for the 2026 season python -m sportstime_parser scrape your_sport --season 2026 ``` ``` What the robot does: [Robot wakes up] | v "I need to find your_sport games!" | v [Goes to ESPN website] | v "Found 500 games! Let me organize them..." | v [Saves to Scripts/output/games_your_sport_2026.json] | v "All done! Here's what I found!" ``` --- ## Step 6: Upload to CloudKit Now let's put those games in the magic cloud box: ```bash # Upload your sport's data to CloudKit python -m sportstime_parser upload your_sport --season 2026 ``` ``` What happens: [Your Computer] | | "Here are 500 games!" v [CloudKit (Magic Cloud Box)] | | "Got them! I'll tell all the apps!" v [Everyone's App Gets the Games!] ``` --- ## The Complete Data Journey ``` YOU (The Data Collector) | | Step 4: Create scraper v +------------------------------------------+ | Python Scraper Robot | | | | 1. Goes to ESPN/league websites | | 2. Collects all game schedules | | 3. Organizes them nicely | | 4. Saves to JSON files | +------------------------------------------+ | | Step 6: Upload v +------------------------------------------+ | CloudKit (Magic Cloud Box) | | | | Stores: | | - Sport definition (Step 2) | | - Teams | | - Stadiums | | - Game schedules | +------------------------------------------+ | | App syncs automatically v +------------------------------------------+ | User's iPhone | | | | "Look! There's a new sport with | | 500 games to explore!" | +------------------------------------------+ ``` --- ## Troubleshooting: My Games Aren't Showing Up! ### Scraper Problems 1. **"No games found"** - Check if the source website changed its format - Try a different source (ESPN, official site, etc.) - Look at the validation report in `Scripts/output/` 2. **"Rate limited"** - The website said "slow down!" - wait a bit and try again - The scraper has built-in delays, but sometimes sites are extra strict 3. **"Authentication error"** - For CloudKit upload, make sure you have `CLOUDKIT_KEY_ID` and `CLOUDKIT_PRIVATE_KEY_PATH` set - Get credentials from Apple Developer Portal ### Useful Commands ```bash # Check what data you have python -m sportstime_parser status # Validate your scraped data python -m sportstime_parser validate your_sport --season 2026 # Retry failed uploads python -m sportstime_parser retry your_sport --season 2026 # Clear upload state and start fresh python -m sportstime_parser clear your_sport --season 2026 ``` --- ## Quick Reference: Adding a Sport End-to-End ``` +--------------------------------------------------+ | COMPLETE SPORT ADDITION CHECKLIST | +--------------------------------------------------+ | | | PART 1: CloudKit (Sport Definition) | | [ ] 1. Open CloudKit Dashboard | | [ ] 2. Create "Sport" record with all fields | | [ ] 3. Save record | | | | PART 2: Python Scraper (Game Data) | | [ ] 4a. Add to SUPPORTED_SPORTS in config.py | | [ ] 4b. Add EXPECTED_GAME_COUNTS in config.py | | [ ] 4c. Create scraper class in scrapers/ | | [ ] 4d. Register in cli.py get_scraper() | | [ ] 5. Run: scrape your_sport --season 2026 | | [ ] 6. Run: upload your_sport --season 2026 | | | | VERIFY: | | [ ] 7. Open app and pull to refresh | | [ ] 8. See your new sport with games! | | | +--------------------------------------------------+ ``` --- **Now you're a pro! The End (for real this time)!** --- *Document created: January 2026* *For the SportsTime app dynamic sports feature*