Initial commit: SportsTime trip planning app
- 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>
This commit is contained in:
275
Scripts/import_to_cloudkit.swift
Normal file
275
Scripts/import_to_cloudkit.swift
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user