Skip to content

Commit c8954e6

Browse files
authored
Merge pull request #97 from playola-radio/develop
0.19.0
2 parents ecd9b19 + d2580c7 commit c8954e6

6 files changed

Lines changed: 813 additions & 162 deletions

File tree

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,50 @@ All notable changes to PlayolaPlayer are documented here. This project follows
44
[Semantic Versioning](https://semver.org/). Versions correspond to git tags,
55
which Swift Package Manager consumers pin to.
66

7+
## 0.19.0
8+
9+
### Added
10+
11+
- **Bounded retry/backoff on the initial schedule fetch.** `play(stationId:)`
12+
now retries a failed initial `GET /v1/stations/{id}/schedule` up to 3 times
13+
with exponential backoff (0.5s / 1s / 2s) before giving up, matching the
14+
recovery behavior the player already used for spin loading and the ongoing
15+
schedule poll. Only transient failures are retried — server `5xx` responses
16+
and an explicit allow-list of connectivity `URLError`s (timeouts, host/DNS,
17+
connection-lost, etc.). Permanent failures (`404`, decode errors, empty
18+
schedules, and non-connectivity `URLError`s such as `.badURL`) still fail
19+
fast. This means a transient backend outage no longer instantly fails a
20+
station start with no automatic recovery.
21+
22+
### Changed
23+
24+
- **`PlayolaStationPlayer.State` gained a terminal `.error(StationPlayerError)`
25+
case.** A failed `play(...)` now emits `.loading(0)` when the attempt begins
26+
and `.error(_)` when it fails terminally (instead of throwing without ever
27+
changing `state`, which could leave consumers stuck on a loading spinner with
28+
no signal). `play(...)` still `throws` as before — the new state is emitted
29+
*in addition to* the thrown error. Cancellation does not produce an `.error`
30+
state.
31+
32+
**Source-breaking for consumers** that `switch` exhaustively over `State`:
33+
add a `case .error` (or `default`) arm. A typical handler treats `.error` as
34+
a recoverable, retry-able state (show the message, let the user tap play
35+
again). `StationPlayerError` is now `Sendable`.
36+
37+
- **State transitions are now supersession-safe (last `play()` wins).** Each
38+
`play(...)`/`stop()` takes a new internal generation; work from a prior
39+
attempt (its scheduling loop, or a spin whose audio starts later) can no
40+
longer publish state into a newer attempt. This closes a race where a
41+
lingering session could overwrite a freshly-emitted `.error` (or `.idle`)
42+
with `.playing`. Behavioral note: starting a new `play()` supersedes the
43+
previous session immediately, so a *failed* station switch lands on `.error`
44+
rather than rolling back to the previously-playing station.
45+
46+
- **`playNow(from:to:)` and `schedulePlay(at:)` are now `async`.** Both `public`
47+
methods changed from synchronous to `async` so their audio work can run off
48+
the main thread. **Source-breaking for direct callers**: add `await` at the
49+
call site (callers must already be in an `async` context, e.g. a `Task`).
50+
751
## 0.18.0
852

953
### Added

PlayolaPlayerExample/PlayolaPlayerExample/ContentView.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ struct ContentView: View {
129129
.font(.caption)
130130
.foregroundColor(.white.opacity(0.6))
131131
}
132+
} else if case .error(let error) = player.state {
133+
VStack(spacing: 8) {
134+
Image(systemName: "exclamationmark.triangle.fill")
135+
.foregroundColor(.yellow)
136+
Text(error.errorDescription ?? "Couldn't start the station")
137+
.font(.subheadline)
138+
.multilineTextAlignment(.center)
139+
.foregroundColor(.white.opacity(0.8))
140+
Text("Tap play to try again")
141+
.font(.caption)
142+
.foregroundColor(.white.opacity(0.6))
143+
}
132144
} else {
133145
Text("Ready to Play")
134146
.font(.title3)
@@ -237,12 +249,13 @@ struct ContentView: View {
237249
case .playing:
238250
// Stop playing
239251
await player.stop()
240-
case .idle:
241-
// Start playing
252+
case .idle, .error:
253+
// Start (or retry after a failed start)
242254
do {
243255
try await player.play(stationId: selectedStationId)
244256
} catch {
245-
// Handle errors gracefully (including cancellation during loading)
257+
// Handle errors gracefully (including cancellation during loading).
258+
// The terminal .error state is surfaced via player.state above.
246259
print("Failed to start playback: \(error)")
247260
}
248261
}
@@ -493,6 +506,8 @@ func buttonColor(for state: PlayolaStationPlayer.State) -> Color {
493506
return Color.red
494507
case .idle:
495508
return Color.green
509+
case .error:
510+
return Color.green
496511
}
497512
}
498513

@@ -504,6 +519,8 @@ func buttonIcon(for state: PlayolaStationPlayer.State) -> String {
504519
return "stop.fill"
505520
case .idle:
506521
return "play.fill"
522+
case .error:
523+
return "arrow.clockwise"
507524
}
508525
}
509526

0 commit comments

Comments
 (0)