Release v5.21.0#1552
Merged
Merged
Conversation
* Update multi-server work for current develop Brings the branch up to date with develop (Core Data v11 migration, custom-headers PR #1513, sticky-sort, end-of-chapter fix, etc.) and folds in a few iterations on top: - A single \"Media Servers\" entry in the import menu in place of the separate Jellyfin / AudiobookShelf items. Lists every saved server from both integrations with type icons; \"Add Server\" picks the type and goes through the connection flow. - Tap-a-server presents the per-integration library browser as a sheet on top of the unified list, with a leading \"Media Servers\" back button that closes the inner sheet and lands you back on the list. Earlier rework tried to push via NavigationStack but pushing a TabView in a NavigationStack auto-pops on iOS 26 — sheet-on-sheet sidesteps that entirely. - Library picker no longer traps you when there are 2+ libraries and none chosen yet (Cancel is unconditional and falls back to dismissing the whole sheet when there's no library to dismiss to). - Connection-form xmark closes the form instead of dismissing the whole integration view. - Empty-state rows restyled to match the populated server-row layout so they read as tappable. Brand strings consolidated through ServerType.displayName, fonts unified on bpFont. * Jellyfin: preserve customHeaders when switching/deleting connections activateConnection and deleteConnection were calling createClient without passing the active connection's customHeaders, so any server behind a Cloudflare Access (or similar) proxy stopped responding after the user switched servers or signed out of one of several — the next request went out missing the required CF-Access-Client-* headers and was rejected until the app was relaunched (the only path that did pass them was reloadConnections at launch). Centralize the client-rebuild in rebuildClient(for:) so future call sites can't drop headers, and capture the old client before mutating state so the fire-and-forget signOut doesn't race the new client assignment. * Integration root: don't auto-push to add-server form on load failure When fetchLibraries (or the equivalent ABS call) threw, the resulting alert had a single OK button that set showConnectionForm = true. That flow is the right one for "the user wants to sign in again" but it's the wrong one for everything else (transient network, expired token, custom-header proxy hiccup). Users who hit it would re-add their server and end up with a duplicate because the byte-exact URL dedup in the connection service didn't treat trailing-slash / scheme variants as the same connection. Replace the single OK with three explicit buttons — Sign In (the old behavior, now opt-in), Retry (re-call loadLibraries), and Cancel (close the integration sheet, return to the Media Servers list). Same fix on both JellyfinRootView and AudiobookShelfRootView. New "integration_retry_button" localization key seeded with English literals across all 26 .lproj files matching the rest of the fork's localization pattern. * Make connect/sign-in tasks cancellable on sheet dismissal IntegrationConnectionView's onConnect / onSignIn / onStartQuickConnect each kicked off an unstructured Task that the view had no handle to. If the user dismissed the sheet (swipe-to-dismiss or hitting the close button) while a sign-in was in flight, the Task kept running and could persist a connection to the service after the user thought they gave up on the operation. Store each action's Task in @State and cancel it from .onDisappear. Catch CancellationError separately so the dismissal doesn't get surfaced as an alert. The View-side cancel alone isn't sufficient — Swift cooperative cancellation only flags the Task, it doesn't synchronously interrupt an awaiting call, and the service methods kept persisting after the network call returned. Add try Task.checkCancellation() inside JellyfinConnectionService.signIn(username:password:...) and AudiobookShelfConnectionService.signIn(...) immediately after the auth round-trip returns, so the persist step is skipped on cancel. The matching call inside signInWithQuickConnect was already added in the Quick-Connect cancel-race fix. * AudiobookShelf: trim credentials and normalize the server URL Two small input-validation gaps: H18 — pingServer accepts a schemeless string like "abs.example.com" verbatim because URL(string:) parses it as a relative path. The subsequent appendingPathComponent("ping") yields "abs.example.com/ping" which URLSession returns an opaque .unsupportedURL for, and the user has no idea they needed to prepend https://. Normalize in the view model before calling pingServer: trim whitespace, prepend https:// if no scheme. Mirror the result back into the form field so the user can see what we actually used. H19 — Username and password were passed to ABS auth verbatim, so an autocorrect-inserted trailing space on the username is enough to fail otherwise-correct credentials with a 401. Trim both in handleSignInAction. * Canonicalize URLs before deduplicating saved connections Foundation URL equality is byte-exact, so https://server vs https://server/ vs https://server:443 vs https://Server were treated as four different connections by the deduplication in JellyfinConnectionService and AudiobookShelfConnectionService. Users hitting the C2 error alert in either integration would commonly re-add their server slightly differently (often with a trailing slash) and end up with two saved entries, one of which was broken. Add URL.canonicalDedupKey to BookPlayerKit's URL extension: lowercase scheme/host, drop default ports (80/443), strip the trailing slash on non-root paths, and drop userinfo/query/fragment. Both services' removeAll-then-append dedup blocks now compare canonical keys. * Real 401 re-auth flow (C2 architectural) Token-expiry / mid-session-unauthorized had no first-class handling — 401/403 propagated as a generic load failure, the alert offered a single OK that pushed the user into the add-server form, and users who took it ended up with duplicate connections (the C2-minimum fix just rearranged the alert buttons to stop the worst case). Distinguish session-expired from generic failures end to end: - New IntegrationError.sessionExpired(serverName:) carries the offending server name so the alert message is specific ("Your session for Library Server expired. Sign in again to continue."). - Jellyfin: wrap the api-client `send<T>` to catch APIError.unacceptableStatusCode(401|403) and throw .sessionExpired — only when there's actually a saved connection (so pre-sign-in probes inside `findServer` aren't mis-classified). - ABS: centralize HTTP status validation in validateAuthenticatedResponse(_:) and use it across every authenticated data-fetch site (fetchLibraries, fetchItems, fetchItemDetails, search, …). Same connection-presence guard. The signIn path keeps its existing 401 → URLError(.userAuthenticationRequired) mapping ("wrong password" is a different UX than "your session expired"). - Both RootView alerts (JellyfinRootView, AudiobookShelfRootView) special-case `IntegrationError.isSessionExpired`: show Sign In + Cancel only, skip the Retry button that's meaningless when the token's revoked. - Both signIn implementations now preserve the existing connection's id and selectedLibraryId when re-authing on the same (canonical-url, userID) pair — so when the user signs in again they land back on the same library context they had before, not a fresh record. This is the half of the fix that makes "Sign In again" actually transparent UX rather than "you lost your saved library choice". New localization key `integration_error_session_expired` seeded with English literals across all 26 .lproj files. * Address feedback * Replace settings options * Address more feedback * Update localizations * Address feedback * change icon --------- Co-authored-by: Gianni Carlo <[email protected]>
…ss (#1524) * fix: don't crash when audio session activation fails PlayerManager.play(autoPlayed:) called fatalError() if either setCategory or setActive(true) threw. Background auto-play (CarPlay, headphones, lock-screen, scheduled) routinely hits cases where another process holds the audio session (phone call, navigation, voice memo) or the OS denies activation, and the app would hard-crash with EXC_BREAKPOINT in libswiftCore from the fatalError trap. Replace with a log-and-return: NSLog the underlying error, clear the queued-playback flag, bail out of this play attempt. A missed auto-play is strictly better than the app dying in the background. Crash signature: TestFlight feedback build 5.19.0 (20260428053421), iPhone 15 Pro / iOS 26.5, role=Background, in PlayerManager.swift:876. * fix: recover when audio session is contested instead of bailing silently Follow-up to e66eb62. Make the recovery match how iOS audio apps normally behave: bind the interruption observer before attempting activation, and on failure keep playbackQueued = true so the existing handleAudioInterruptions path will retry play(autoPlayed:) when iOS reports the session is free again (call ended, navigation done, voice memo dismissed). Previously the catch left playbackQueued = nil, so a contested auto-play was a dead-end until the user manually pressed play. * fix: gate audio-session recovery to beta and retry with backoff Addresses review feedback on #1524. The previous recovery relied solely on AVAudioSession's `.ended` interrupt notification to retry play. That notification only fires for sessions that were already active and got interrupted — it does not fire when `setActive(true)` itself throws, which is precisely the unrecoverable state described in review (force-quit was the only fix). Without an alternative trigger the UI would sit in the queued/loading state indefinitely. Changes: - Gate the non-fatal path to beta (TestFlight / DEBUG) builds. Release builds keep the original `fatalError` so the App Store version isn't masked while the recovery is unproven. - Add `scheduleAudioSessionRecovery`: bounded retry at 1s/3s/8s using the deactivate-then-reactivate pattern that's known to unstick the contested-session state. On success, resume playback; on exhaustion, clear `playbackQueued` and surface an alert with the same actionable hint the crash provided ("close and reopen"). - Cancel any pending retry from `play`, `pause`, and `stopPlayback` so user-initiated state changes aren't clobbered by a late retry. * fix: use AppEnvironment.isTestFlight for the recovery gate Replaces a duplicated TestFlight check with the existing AppEnvironment.isTestFlight helper used elsewhere in the project (TipJar, AccountService, PurchasesManager). Behavior change: DEBUG builds no longer take the recovery path — they keep the original fatalError so the failure is loud during development. Only true TestFlight installs run the bounded retry, which matches the "just to see if we get reports" intent of the gate. * simplify report * cleanup --------- Co-authored-by: Gianni Carlo <[email protected]>
* Fix searching on iOS 18 * Address feedback
* Fix updating last played widget after delete * address feedback * address feedback
* Add support for total remaining time in chapter context * address feedback * address feedback 2
Add playing specific book with app intents
Add chapter threshold logic for prev chapter button
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
release notes:
Improvements
– Add multi-server support for Jellyfin and AudiobookShelf
– You can now configure the player controls to show the total remaining time of a book while having the chapter context enabled
– Improve chapter list parsing when importing books
– You can now ask Siri to play a book, or create an app shortcut to play any specific book from your library
– Add the same chapter-threshold stop we have when rewinding, to the previous-chapter button
Bugfixes
– Fix search in iOS 18
– Fix updating last played books widget after deleting a book
Special thanks to:
– klikh (sponsor)
– Canadian Organization of the Blind and DeafBlind and Matthew for their contributions to this update
If you experience any issues, please reach us at [email protected]