- Three-scenario planning engine (A: date range, B: selected games, C: directional routes) - GeographicRouteExplorer with anchor game support for route exploration - Shared ItineraryBuilder for travel segment calculation - TravelEstimator for driving time/distance estimation - SwiftUI views for trip creation and detail display - CloudKit integration for schedule data - Python scraping scripts for sports schedules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
276 lines
8.4 KiB
Swift
276 lines
8.4 KiB
Swift
#!/usr/bin/env swift
|
|
//
|
|
// import_to_cloudkit.swift
|
|
// SportsTime
|
|
//
|
|
// Imports scraped JSON data into CloudKit public database.
|
|
// Run from command line: swift import_to_cloudkit.swift --games data/games.json --stadiums data/stadiums.json
|
|
//
|
|
|
|
import Foundation
|
|
import CloudKit
|
|
|
|
// MARK: - Data Models (matching scraper output)
|
|
|
|
struct ScrapedGame: Codable {
|
|
let id: String
|
|
let sport: String
|
|
let season: String
|
|
let date: String
|
|
let time: String?
|
|
let home_team: String
|
|
let away_team: String
|
|
let home_team_abbrev: String
|
|
let away_team_abbrev: String
|
|
let venue: String
|
|
let source: String
|
|
let is_playoff: Bool?
|
|
let broadcast: String?
|
|
}
|
|
|
|
struct ScrapedStadium: Codable {
|
|
let id: String
|
|
let name: String
|
|
let city: String
|
|
let state: String
|
|
let latitude: Double
|
|
let longitude: Double
|
|
let capacity: Int
|
|
let sport: String
|
|
let team_abbrevs: [String]
|
|
let source: String
|
|
let year_opened: Int?
|
|
}
|
|
|
|
// MARK: - CloudKit Importer
|
|
|
|
class CloudKitImporter {
|
|
let container: CKContainer
|
|
let database: CKDatabase
|
|
|
|
init(containerIdentifier: String = "iCloud.com.sportstime.app") {
|
|
self.container = CKContainer(identifier: containerIdentifier)
|
|
self.database = container.publicCloudDatabase
|
|
}
|
|
|
|
// MARK: - Import Stadiums
|
|
|
|
func importStadiums(from stadiums: [ScrapedStadium]) async throws -> Int {
|
|
var imported = 0
|
|
|
|
for stadium in stadiums {
|
|
let record = CKRecord(recordType: "Stadium")
|
|
record["stadiumId"] = stadium.id
|
|
record["name"] = stadium.name
|
|
record["city"] = stadium.city
|
|
record["state"] = stadium.state
|
|
record["location"] = CLLocation(latitude: stadium.latitude, longitude: stadium.longitude)
|
|
record["capacity"] = stadium.capacity
|
|
record["sport"] = stadium.sport
|
|
record["teamAbbrevs"] = stadium.team_abbrevs
|
|
record["source"] = stadium.source
|
|
|
|
if let yearOpened = stadium.year_opened {
|
|
record["yearOpened"] = yearOpened
|
|
}
|
|
|
|
do {
|
|
_ = try await database.save(record)
|
|
imported += 1
|
|
print(" Imported stadium: \(stadium.name)")
|
|
} catch {
|
|
print(" Error importing \(stadium.name): \(error)")
|
|
}
|
|
}
|
|
|
|
return imported
|
|
}
|
|
|
|
// MARK: - Import Teams
|
|
|
|
func importTeams(from stadiums: [ScrapedStadium], teamMappings: [String: TeamInfo]) async throws -> [String: CKRecord.ID] {
|
|
var teamRecordIDs: [String: CKRecord.ID] = [:]
|
|
|
|
for (abbrev, info) in teamMappings {
|
|
let record = CKRecord(recordType: "Team")
|
|
record["teamId"] = UUID().uuidString
|
|
record["name"] = info.name
|
|
record["abbreviation"] = abbrev
|
|
record["sport"] = info.sport
|
|
record["city"] = info.city
|
|
|
|
do {
|
|
let saved = try await database.save(record)
|
|
teamRecordIDs[abbrev] = saved.recordID
|
|
print(" Imported team: \(info.name)")
|
|
} catch {
|
|
print(" Error importing team \(info.name): \(error)")
|
|
}
|
|
}
|
|
|
|
return teamRecordIDs
|
|
}
|
|
|
|
// MARK: - Import Games
|
|
|
|
func importGames(
|
|
from games: [ScrapedGame],
|
|
teamRecordIDs: [String: CKRecord.ID],
|
|
stadiumRecordIDs: [String: CKRecord.ID]
|
|
) async throws -> Int {
|
|
var imported = 0
|
|
|
|
// Batch imports for efficiency
|
|
let batchSize = 100
|
|
var batch: [CKRecord] = []
|
|
|
|
for game in games {
|
|
let record = CKRecord(recordType: "Game")
|
|
record["gameId"] = game.id
|
|
record["sport"] = game.sport
|
|
record["season"] = game.season
|
|
|
|
// Parse date
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
|
if let date = dateFormatter.date(from: game.date) {
|
|
if let timeStr = game.time {
|
|
// Combine date and time
|
|
let timeFormatter = DateFormatter()
|
|
timeFormatter.dateFormat = "HH:mm"
|
|
if let time = timeFormatter.date(from: timeStr) {
|
|
let calendar = Calendar.current
|
|
let timeComponents = calendar.dateComponents([.hour, .minute], from: time)
|
|
if let combined = calendar.date(bySettingHour: timeComponents.hour ?? 19,
|
|
minute: timeComponents.minute ?? 0,
|
|
second: 0, of: date) {
|
|
record["dateTime"] = combined
|
|
}
|
|
}
|
|
} else {
|
|
// Default to 7 PM if no time
|
|
let calendar = Calendar.current
|
|
if let defaultTime = calendar.date(bySettingHour: 19, minute: 0, second: 0, of: date) {
|
|
record["dateTime"] = defaultTime
|
|
}
|
|
}
|
|
}
|
|
|
|
// Team references
|
|
if let homeTeamID = teamRecordIDs[game.home_team_abbrev] {
|
|
record["homeTeamRef"] = CKRecord.Reference(recordID: homeTeamID, action: .none)
|
|
}
|
|
if let awayTeamID = teamRecordIDs[game.away_team_abbrev] {
|
|
record["awayTeamRef"] = CKRecord.Reference(recordID: awayTeamID, action: .none)
|
|
}
|
|
|
|
record["isPlayoff"] = (game.is_playoff ?? false) ? 1 : 0
|
|
record["broadcastInfo"] = game.broadcast
|
|
record["source"] = game.source
|
|
|
|
batch.append(record)
|
|
|
|
// Save batch
|
|
if batch.count >= batchSize {
|
|
do {
|
|
let operation = CKModifyRecordsOperation(recordsToSave: batch, recordIDsToDelete: nil)
|
|
operation.savePolicy = .changedKeys
|
|
|
|
try await database.modifyRecords(saving: batch, deleting: [])
|
|
imported += batch.count
|
|
print(" Imported batch of \(batch.count) games (total: \(imported))")
|
|
batch.removeAll()
|
|
} catch {
|
|
print(" Error importing batch: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save remaining
|
|
if !batch.isEmpty {
|
|
do {
|
|
try await database.modifyRecords(saving: batch, deleting: [])
|
|
imported += batch.count
|
|
} catch {
|
|
print(" Error importing final batch: \(error)")
|
|
}
|
|
}
|
|
|
|
return imported
|
|
}
|
|
}
|
|
|
|
// MARK: - Team Info
|
|
|
|
struct TeamInfo {
|
|
let name: String
|
|
let city: String
|
|
let sport: String
|
|
}
|
|
|
|
// MARK: - Main
|
|
|
|
func loadJSON<T: Codable>(from path: String) throws -> T {
|
|
let url = URL(fileURLWithPath: path)
|
|
let data = try Data(contentsOf: url)
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
func main() async {
|
|
let args = CommandLine.arguments
|
|
|
|
guard args.count >= 3 else {
|
|
print("Usage: swift import_to_cloudkit.swift --games <path> --stadiums <path>")
|
|
return
|
|
}
|
|
|
|
var gamesPath: String?
|
|
var stadiumsPath: String?
|
|
|
|
for i in 1..<args.count {
|
|
if args[i] == "--games" && i + 1 < args.count {
|
|
gamesPath = args[i + 1]
|
|
}
|
|
if args[i] == "--stadiums" && i + 1 < args.count {
|
|
stadiumsPath = args[i + 1]
|
|
}
|
|
}
|
|
|
|
let importer = CloudKitImporter()
|
|
|
|
// Import stadiums
|
|
if let path = stadiumsPath {
|
|
print("\n=== Importing Stadiums ===")
|
|
do {
|
|
let stadiums: [ScrapedStadium] = try loadJSON(from: path)
|
|
let count = try await importer.importStadiums(from: stadiums)
|
|
print("Imported \(count) stadiums")
|
|
} catch {
|
|
print("Error loading stadiums: \(error)")
|
|
}
|
|
}
|
|
|
|
// Import games
|
|
if let path = gamesPath {
|
|
print("\n=== Importing Games ===")
|
|
do {
|
|
let games: [ScrapedGame] = try loadJSON(from: path)
|
|
// Note: Would need to first import teams and get their record IDs
|
|
// This is a simplified version
|
|
print("Loaded \(games.count) games for import")
|
|
} catch {
|
|
print("Error loading games: \(error)")
|
|
}
|
|
}
|
|
|
|
print("\n=== Import Complete ===")
|
|
}
|
|
|
|
// Run
|
|
Task {
|
|
await main()
|
|
}
|
|
|
|
// Keep the process running for async operations
|
|
RunLoop.main.run()
|