Files
MatchLiveTv/native/ios/MatchLiveTv/Data/API/ApiDtos.swift
Emiliano Frascaro 585332e32e Aggiunge app iOS nativa con parità multi-sport e test unitari.
Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 21:37:32 +02:00

476 lines
14 KiB
Swift

import Foundation
struct LoginRequest: Encodable {
let email: String
let password: String
}
struct RefreshRequest: Encodable {
let refreshToken: String
}
struct LoginResponse: Decodable {
let user: UserDto
let accessToken: String
let refreshToken: String
func toDomain() -> (User, AuthTokens) {
(user.toDomain(), AuthTokens(accessToken: accessToken, refreshToken: refreshToken))
}
}
struct UserDto: Decodable {
let id: String
let email: String
let name: String
let role: String?
func toDomain() -> User {
User(id: id, email: email, name: name, role: role ?? "coach")
}
}
struct SportDto: Decodable {
let key: String
let label: String
let board: String
let overlay: String
let allowedOverlays: [String]?
let defaultRules: [String: Int]?
func toDomain() -> SportOption {
SportOption(
key: key,
label: label,
board: board,
overlay: overlay,
allowedOverlays: allowedOverlays ?? [],
defaultRules: defaultRules ?? [:]
)
}
}
struct TeamDto: Decodable {
let id: String
let name: String
let sport: String?
let sportKey: String?
let boardType: String?
let canStream: Bool?
let clubName: String?
let youtubeEnabled: Bool?
let youtubeConnected: Bool?
let youtubeSelectable: Bool?
let youtubeChannelTitle: String?
let youtubeUsesPlatformChannel: Bool?
let youtubeTeamChannelTitle: String?
let planName: String?
let recordingsEnabled: Bool?
let phoneDownloadEnabled: Bool?
let billingUrl: String?
let staffManageUrl: String?
let logoUrl: String?
let primaryColor: String?
let secondaryColor: String?
func toDomain() -> Team {
Team(
id: id,
name: name,
sport: sportKey ?? sport ?? "pallavolo",
sportKey: sportKey ?? sport ?? "pallavolo",
boardType: boardType ?? "volley",
clubName: clubName,
logoUrl: logoUrl,
primaryColor: primaryColor,
secondaryColor: secondaryColor,
canStream: canStream ?? true,
youtubeEnabled: youtubeEnabled ?? false,
youtubeConnected: youtubeConnected ?? false,
youtubeSelectable: youtubeSelectable ?? false,
youtubeChannelTitle: youtubeChannelTitle,
youtubeUsesPlatformChannel: youtubeUsesPlatformChannel ?? false,
youtubeTeamChannelTitle: youtubeTeamChannelTitle,
planName: planName,
recordingsEnabled: recordingsEnabled ?? false,
phoneDownloadEnabled: phoneDownloadEnabled ?? false,
billingUrl: billingUrl,
staffManageUrl: staffManageUrl
)
}
}
struct MatchDto: Decodable {
let id: String
let teamId: String
let teamName: String?
let opponentName: String
let location: String?
let scheduledAt: String?
let setsToWin: Int?
let sportKey: String?
let sport: String?
let sportLabel: String?
let boardType: String?
let overlayKind: String?
let effectiveOverlayKind: String?
let category: String?
let scoringRules: ScoringRulesBody?
let activeSessionId: String?
let activeSessionStatus: String?
let streamCompleted: Bool?
let coachHubVisible: Bool?
let homePrimaryColor: String?
let homeSecondaryColor: String?
let homeLogoUrl: String?
let opponentPrimaryColor: String?
let opponentLogoUrl: String?
func toDomain() -> Match {
Match(
id: id,
teamId: teamId,
teamName: teamName ?? "",
opponentName: opponentName,
location: location,
scheduledAt: scheduledAt,
sportKey: sportKey ?? sport ?? "pallavolo",
sportLabel: sportLabel,
boardType: boardType ?? "volley",
overlayKind: overlayKind,
effectiveOverlayKind: effectiveOverlayKind ?? overlayKind ?? "volley",
setsToWin: setsToWin ?? 3,
category: category,
scoringRules: scoringRules?.toDomain(),
activeSessionId: activeSessionId,
activeSessionStatus: activeSessionStatus,
streamCompleted: streamCompleted ?? false,
coachHubVisible: coachHubVisible,
homePrimaryColor: homePrimaryColor,
homeSecondaryColor: homeSecondaryColor,
homeLogoUrl: homeLogoUrl,
opponentPrimaryColor: opponentPrimaryColor,
opponentLogoUrl: opponentLogoUrl
)
}
}
struct StreamSessionDto: Decodable {
let id: String
let matchId: String
let status: String?
let platform: String?
let rtmpIngestUrl: String?
let hlsPlaybackUrl: String?
let watchPageUrl: String?
let shareUrl: String?
let youtubeWatchUrl: String?
let youtubeReady: Bool?
let privacyStatus: String?
let targetBitrate: Int?
let targetFps: Int?
let startedAt: String?
let score: ScoreStateDto?
func toDomain() -> StreamSession {
StreamSession(
id: id,
matchId: matchId,
status: status ?? "idle",
platform: platform ?? "matchlivetv",
rtmpIngestUrl: rtmpIngestUrl,
hlsPlaybackUrl: hlsPlaybackUrl,
watchPageUrl: watchPageUrl,
shareUrl: shareUrl,
youtubeWatchUrl: youtubeWatchUrl,
youtubeReady: youtubeReady ?? false,
privacyStatus: privacyStatus ?? "public",
targetBitrate: targetBitrate ?? 2_500_000,
targetFps: targetFps ?? 30,
startedAt: startedAt,
score: score?.toDomain()
)
}
}
struct SetPartialDto: Codable {
let set: Int?
let home: Int?
let away: Int?
}
/// Decodifica tollerante per campi JSONB (`score_states.data`).
private enum FlexibleJSON: Decodable {
case int(Int)
case bool(Bool)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let v = try? container.decode(Bool.self) { self = .bool(v); return }
if let v = try? container.decode(Int.self) { self = .int(v); return }
if let v = try? container.decode(String.self) { self = .string(v); return }
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value")
}
var intValue: Int? {
switch self {
case .int(let v): return v
case .string(let s): return Int(s)
case .bool(let v): return v ? 1 : 0
}
}
var boolValue: Bool? {
switch self {
case .bool(let v): return v
case .int(let v): return v != 0
case .string(let s): return (s as NSString).boolValue
}
}
}
struct ScoreStateDto: Decodable {
let boardType: String?
let homeSets: Int?
let awaySets: Int?
let homePoints: Int?
let awayPoints: Int?
let currentSet: Int?
let setPartials: [SetPartialDto]?
let timeoutHome: Bool?
let timeoutAway: Bool?
let period: String?
let clockSecs: Int?
let clockRunning: Bool?
enum CodingKeys: String, CodingKey {
case boardType = "board_type"
case homeSets = "home_sets"
case awaySets = "away_sets"
case homePoints = "home_points"
case awayPoints = "away_points"
case currentSet = "current_set"
case setPartials = "set_partials"
case timeoutHome = "timeout_home"
case timeoutAway = "timeout_away"
case period
case clockSecs = "clock_secs"
case clockRunning = "clock_running"
case data
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
boardType = try c.decodeIfPresent(String.self, forKey: .boardType)
homeSets = Self.decodeInt(c, key: .homeSets)
awaySets = Self.decodeInt(c, key: .awaySets)
homePoints = Self.decodeInt(c, key: .homePoints)
awayPoints = Self.decodeInt(c, key: .awayPoints)
currentSet = Self.decodeInt(c, key: .currentSet)
setPartials = try c.decodeIfPresent([SetPartialDto].self, forKey: .setPartials)
timeoutHome = Self.decodeBool(c, key: .timeoutHome)
timeoutAway = Self.decodeBool(c, key: .timeoutAway)
period = Self.decodeString(c, key: .period)
nestedData = try c.decodeIfPresent([String: FlexibleJSON].self, forKey: .data)
clockSecs = Self.decodeInt(c, key: .clockSecs) ?? nestedData?["clock_secs"]?.intValue
clockRunning = Self.decodeBool(c, key: .clockRunning) ?? nestedData?["clock_running"]?.boolValue
}
private let nestedData: [String: FlexibleJSON]?
private static func decodeInt(_ c: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> Int? {
if let v = try? c.decodeIfPresent(Int.self, forKey: key) { return v }
if let s = try? c.decodeIfPresent(String.self, forKey: key), let v = Int(s) { return v }
return nil
}
private static func decodeBool(_ c: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> Bool? {
if let v = try? c.decodeIfPresent(Bool.self, forKey: key) { return v }
if let i = try? c.decodeIfPresent(Int.self, forKey: key) { return i != 0 }
return nil
}
private static func decodeString(_ c: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> String? {
if let s = try? c.decodeIfPresent(String.self, forKey: key) { return s }
if let i = try? c.decodeIfPresent(Int.self, forKey: key) { return String(i) }
return nil
}
func toDomain() -> ScoreState {
let board = boardType ?? "volley"
let homePts = homePoints ?? nestedData?["home_score"]?.intValue ?? 0
let awayPts = awayPoints ?? nestedData?["away_score"]?.intValue ?? 0
let overtime = nestedData?["overtime"]?.boolValue ?? false
return ScoreState(
homeSets: homeSets ?? 0,
awaySets: awaySets ?? 0,
homePoints: homePts,
awayPoints: awayPts,
currentSet: max(currentSet ?? 1, 1),
setPartials: (setPartials ?? []).map {
SetPartial(set: $0.set ?? 1, home: $0.home ?? 0, away: $0.away ?? 0)
},
timeoutHome: timeoutHome ?? false,
timeoutAway: timeoutAway ?? false,
boardType: board,
period: parsePeriodNumber(period, currentSet: currentSet),
periodLabel: period,
clockSecs: clockSecs ?? 0,
clockRunning: clockRunning ?? false,
overtime: overtime
)
}
private func parsePeriodNumber(_ label: String?, currentSet: Int?) -> Int {
if let text = label {
if let m = text.range(of: #"Q(\d+)"#, options: .regularExpression) {
let num = text[m].dropFirst()
if let v = Int(num) { return v }
}
if let v = Int(text), v > 0 { return v }
}
return max(currentSet ?? 1, 1)
}
}
struct ScoreActionRequest: Encodable {
let scoreAction: String
}
struct ScoreSyncRequest: Encodable {
let homeSets: Int
let awaySets: Int
let homePoints: Int
let awayPoints: Int
let currentSet: Int
let setPartials: [SetPartialDto]
let clockSecs: Int?
let clockRunning: Bool?
let period: Int?
let homeScore: Int?
let awayScore: Int?
}
struct CreateSessionRequest: Encodable {
let platform: String
let privacyStatus: String
let qualityPreset: String
let targetBitrate: Int
let targetFps: Int
let youtubeChannel: String?
}
struct ScoringRulesBody: Codable {
let setsToWin: Int?
let pointsPerSet: Int?
let pointsDecidingSet: Int?
let minPointLead: Int?
let periods: Int?
let periodDurationSecs: Int?
let overtimeDurationSecs: Int?
func toDomain() -> ScoringRules {
ScoringRules(
pointsPerSet: pointsPerSet ?? 25,
pointsDecidingSet: pointsDecidingSet ?? 15,
minPointLead: minPointLead ?? 2,
periods: periods ?? 4,
periodDurationSecs: periodDurationSecs ?? 600,
overtimeDurationSecs: overtimeDurationSecs ?? 300
)
}
}
struct UpdateMatchBodyFull: Encodable {
let opponentName: String
let location: String?
let scheduledAt: String?
let setsToWin: Int
let sportKey: String?
let overlayKind: String?
let category: String?
let opponentPrimaryColor: String?
let scoringRules: [String: Int]?
}
struct UpdateMatchRequestFull: Encodable {
let match: UpdateMatchBodyFull
}
struct UpdateTeamBody: Encodable {
let primaryColor: String?
let secondaryColor: String?
}
struct UpdateTeamRequest: Encodable {
let team: UpdateTeamBody
}
struct NetworkTestResponse: Decodable {
let ready: Bool
let targetUploadMbps: Double?
let qualityPreset: String?
let targetBitrate: Int?
let targetFps: Int?
}
struct RegiaLinkResponse: Decodable {
let regiaUrl: String
}
struct CreateMatchBody: Encodable {
let opponentName: String
let sport: String
let setsToWin: Int
let scheduledAt: String?
let location: String?
}
struct CreateMatchRequest: Encodable {
let match: CreateMatchBody
}
struct NetworkTestRequest: Encodable {
let downloadMbps: Double
let uploadMbps: Double
let latencyMs: Int
let networkType: String
}
struct TelemetryRequest: Encodable {
let deviceRole: String
let batteryLevel: Int?
let networkType: String?
let signalStrength: Int?
let currentBitrate: Int?
let targetBitrate: Int?
let fps: Int?
}
extension ScoringRules {
func toBody(setsToWin: Int? = nil) -> ScoringRulesBody {
ScoringRulesBody(
setsToWin: setsToWin,
pointsPerSet: pointsPerSet,
pointsDecidingSet: pointsDecidingSet,
minPointLead: minPointLead,
periods: periods,
periodDurationSecs: periodDurationSecs,
overtimeDurationSecs: overtimeDurationSecs
)
}
func toMap(setsToWin: Int? = nil) -> [String: Int] {
var map: [String: Int] = [:]
if let setsToWin { map["sets_to_win"] = setsToWin }
map["points_per_set"] = pointsPerSet
map["points_deciding_set"] = pointsDecidingSet
map["min_point_lead"] = minPointLead
map["periods"] = periods
map["period_duration_secs"] = periodDurationSecs
map["overtime_duration_secs"] = overtimeDurationSecs
return map
}
}