Show full playoff bracket, mark series results, harden API decoding
Playoffs: - List every round played so far (Round 1 → current) instead of only the current round, on both macOS menu and iPhone - Strike through the eliminated team's tricode in a finished series and drop the now-redundant "(Final … wins)" tag on completed earlier rounds - Refetch the bracket when a finished game implies more completed games than the cached bracket records, so the series score and round no longer get stuck on stale data after cold launch or the NHL bracket endpoint's lag API robustness: - Tolerate optional gameCenterLink/startTimeUTC on TBD playoff matchups so the scoreboard decode no longer aborts - Reject API state regressions via a monotonic FUT→…→OFF progression rank so a brief glitch can't downgrade a finished game back to "-:-"
This commit is contained in:
@@ -40,6 +40,12 @@ struct PlayoffBracket: Codable {
|
||||
return topSeedWins == 4 ? topSeedTeam?.abbrev : bottomSeedTeam?.abbrev
|
||||
}
|
||||
|
||||
/// Abbrev of the eliminated side, nil if the series is ongoing.
|
||||
var loser: String? {
|
||||
guard isOver else { return nil }
|
||||
return topSeedWins == 4 ? bottomSeedTeam?.abbrev : topSeedTeam?.abbrev
|
||||
}
|
||||
|
||||
/// Absolute URL for the NHL.com series page, or nil if the bracket doesn't provide one.
|
||||
var fullSeriesUrl: String? {
|
||||
seriesUrl.map { "https://www.nhl.com\($0)" }
|
||||
@@ -63,7 +69,17 @@ struct PlayoffBracket: Codable {
|
||||
/// All matched series in the current round.
|
||||
var currentRoundSeries: [Series] {
|
||||
guard let round = currentRound else { return [] }
|
||||
return series
|
||||
return matchedSeries(inRound: round)
|
||||
}
|
||||
|
||||
/// Rounds that have at least one matched series, ascending (1…current).
|
||||
var activeRounds: [Int] {
|
||||
Set(series.filter(\.isMatched).map(\.playoffRound)).sorted()
|
||||
}
|
||||
|
||||
/// All matched series in the given round, sorted by series letter.
|
||||
func matchedSeries(inRound round: Int) -> [Series] {
|
||||
series
|
||||
.filter { $0.playoffRound == round && $0.isMatched }
|
||||
.sorted { $0.seriesLetter < $1.seriesLetter }
|
||||
}
|
||||
|
||||
@@ -42,6 +42,27 @@ enum GameState: String, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Monotonic ordering along the FUT→PRE→LIVE→CRIT→OVER→FINAL→OFF progression.
|
||||
/// Used to detect — and reject — API regressions that briefly downgrade a
|
||||
/// finished game back to FUT during the daily focusedDate rollover.
|
||||
var progressionRank: Int {
|
||||
switch self {
|
||||
case .future: return 0
|
||||
case .pre: return 1
|
||||
case .live: return 2
|
||||
case .crit: return 3
|
||||
case .over: return 4
|
||||
case .final_: return 5
|
||||
case .official: return 6
|
||||
}
|
||||
}
|
||||
|
||||
/// Same ranking, addressable by the raw API string. Unknown states get -1
|
||||
/// so any known state replaces them.
|
||||
static func progressionRank(of rawState: String) -> Int {
|
||||
GameState(rawValue: rawState)?.progressionRank ?? -1
|
||||
}
|
||||
|
||||
var pollingInterval: PollingInterval {
|
||||
switch self {
|
||||
case .future:
|
||||
|
||||
@@ -26,8 +26,10 @@ struct Scoreboard: Codable {
|
||||
let season: Int
|
||||
let gameType: Int
|
||||
let gameDate: String
|
||||
let gameCenterLink: String
|
||||
let startTimeUTC: String
|
||||
/// Missing on TBD playoff matchups (opponent not yet decided).
|
||||
let gameCenterLink: String?
|
||||
/// Missing on TBD playoff matchups not yet scheduled.
|
||||
let startTimeUTC: String?
|
||||
let gameState: String
|
||||
let gameScheduleState: String
|
||||
let awayTeam: Team
|
||||
@@ -62,19 +64,23 @@ struct Scoreboard: Codable {
|
||||
}
|
||||
|
||||
var date: Date {
|
||||
ISO8601DateFormatter().date(from: startTimeUTC) ?? .now
|
||||
guard let startTimeUTC else { return .distantFuture }
|
||||
return ISO8601DateFormatter().date(from: startTimeUTC) ?? .now
|
||||
}
|
||||
|
||||
var gameCenterUrl: String {
|
||||
"https://www.nhl.com\(gameCenterLink)"
|
||||
var gameCenterUrl: String? {
|
||||
guard let gameCenterLink else { return nil }
|
||||
return "https://www.nhl.com\(gameCenterLink)"
|
||||
}
|
||||
|
||||
var videocastUrl: String {
|
||||
"https://videocast.nhl.com/game/\(id)/usnded?autoplay=true"
|
||||
}
|
||||
|
||||
/// Time string in ET, right-aligned to 8 chars (e.g. " 9:30 PM", "10:00 PM")
|
||||
/// Time string in ET, right-aligned to 8 chars (e.g. " 9:30 PM", "10:00 PM").
|
||||
/// Empty when the game has no scheduled start (TBD opponent).
|
||||
var startTimeET: String {
|
||||
guard startTimeUTC != nil else { return "" }
|
||||
let raw = date.formatDateET(format: "h:mm a")
|
||||
return raw.count < 8
|
||||
? String(repeating: " ", count: 8 - raw.count) + raw
|
||||
|
||||
Reference in New Issue
Block a user