@wwdrew/expo-spotify-sdk 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -346
- package/android/build.gradle +4 -5
- package/android/libs/SETUP.md +9 -24
- package/android/spotify-native-sdk.gradle +118 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +14 -5
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +38 -64
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteErrorMapping.kt +106 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +2 -7
- package/build/app-remote/index.d.ts +34 -0
- package/build/app-remote/index.d.ts.map +1 -1
- package/build/app-remote/index.js +47 -24
- package/build/app-remote/index.js.map +1 -1
- package/build/auth/index.d.ts.map +1 -1
- package/build/auth/index.js +17 -34
- package/build/auth/index.js.map +1 -1
- package/build/content/index.d.ts.map +1 -1
- package/build/content/index.js +12 -25
- package/build/content/index.js.map +1 -1
- package/build/hooks/index.d.ts +1 -1
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +38 -115
- package/build/hooks/index.js.map +1 -1
- package/build/images/index.d.ts +2 -2
- package/build/images/index.d.ts.map +1 -1
- package/build/images/index.js +12 -25
- package/build/images/index.js.map +1 -1
- package/build/index.d.ts +0 -18
- package/build/index.d.ts.map +1 -1
- package/build/index.js +0 -27
- package/build/index.js.map +1 -1
- package/build/internal/native-errors.d.ts +14 -0
- package/build/internal/native-errors.d.ts.map +1 -0
- package/build/internal/native-errors.js +29 -0
- package/build/internal/native-errors.js.map +1 -0
- package/build/internal/sync-external-store.d.ts +14 -0
- package/build/internal/sync-external-store.d.ts.map +1 -0
- package/build/internal/sync-external-store.js +36 -0
- package/build/internal/sync-external-store.js.map +1 -0
- package/build/player/index.d.ts.map +1 -1
- package/build/player/index.js +14 -27
- package/build/player/index.js.map +1 -1
- package/build/user/index.d.ts +2 -2
- package/build/user/index.d.ts.map +1 -1
- package/build/user/index.js +16 -26
- package/build/user/index.js.map +1 -1
- package/ios/ExpoSpotifyAppDelegate.swift +8 -1
- package/ios/ExpoSpotifySDK.podspec +26 -2
- package/ios/ExpoSpotifySDKModule.swift +16 -1
- package/ios/SpotifyAppRemoteCoordinator.swift +137 -404
- package/ios/SpotifyAppRemoteErrorMapping.swift +110 -0
- package/ios/SpotifyAppRemoteErrors.swift +221 -0
- package/ios/SpotifyAppRemoteMappers.swift +88 -0
- package/ios/spotify-native-sdk-versions.json +12 -0
- package/package.json +13 -5
- package/plugin/index.d.ts +2 -0
- package/plugin/index.js +1 -0
- package/ios/SpotifySDK/Licenses/MPMessagePack-LICENSE.md +0 -20
- package/ios/SpotifySDK/SpotifyiOS.xcframework/Info.plist +0 -40
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +0 -31
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Modules/module.modulemap +0 -5
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +0 -31
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Modules/module.modulemap +0 -5
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/SpotifyiOS +0 -0
|
@@ -1,227 +1,7 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
2
1
|
import Foundation
|
|
3
2
|
import SpotifyiOS
|
|
4
3
|
import UIKit
|
|
5
4
|
|
|
6
|
-
// MARK: — App Remote error types
|
|
7
|
-
|
|
8
|
-
enum AppRemoteError: Error {
|
|
9
|
-
case connectionFailed(String)
|
|
10
|
-
case connectionLost(String)
|
|
11
|
-
case notConnected(String)
|
|
12
|
-
case unknown(String)
|
|
13
|
-
|
|
14
|
-
var code: String {
|
|
15
|
-
switch self {
|
|
16
|
-
case .connectionFailed: return "CONNECTION_FAILED"
|
|
17
|
-
case .connectionLost: return "CONNECTION_LOST"
|
|
18
|
-
case .notConnected: return "NOT_CONNECTED"
|
|
19
|
-
case .unknown: return "UNKNOWN"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
var message: String {
|
|
24
|
-
switch self {
|
|
25
|
-
case .connectionFailed(let m): return m
|
|
26
|
-
case .connectionLost(let m): return m
|
|
27
|
-
case .notConnected(let m): return m
|
|
28
|
-
case .unknown(let m): return m
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/// Bridges an `AppRemoteError` through expo-modules-core's exception system so
|
|
34
|
-
/// JS callers receive a structured `code` and `reason`, not "undefined reason".
|
|
35
|
-
final class AppRemoteException: Exception, @unchecked Sendable {
|
|
36
|
-
private let appRemoteCode: String
|
|
37
|
-
private let appRemoteMessage: String
|
|
38
|
-
|
|
39
|
-
init(_ error: AppRemoteError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
40
|
-
self.appRemoteCode = error.code
|
|
41
|
-
self.appRemoteMessage = error.message
|
|
42
|
-
super.init(file: file, line: line, function: function)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
override var code: String { appRemoteCode }
|
|
46
|
-
override var reason: String { appRemoteMessage }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// MARK: — Player error types
|
|
50
|
-
|
|
51
|
-
enum NativePlayerError: Error {
|
|
52
|
-
case notConnected(String)
|
|
53
|
-
case connectionLost(String)
|
|
54
|
-
case premiumRequired(String)
|
|
55
|
-
case invalidURI(String)
|
|
56
|
-
case invalidParameter(String)
|
|
57
|
-
case operationNotAllowed(String)
|
|
58
|
-
case unknown(String)
|
|
59
|
-
|
|
60
|
-
var code: String {
|
|
61
|
-
switch self {
|
|
62
|
-
case .notConnected: return "NOT_CONNECTED"
|
|
63
|
-
case .connectionLost: return "CONNECTION_LOST"
|
|
64
|
-
case .premiumRequired: return "PREMIUM_REQUIRED"
|
|
65
|
-
case .invalidURI: return "INVALID_URI"
|
|
66
|
-
case .invalidParameter: return "INVALID_PARAMETER"
|
|
67
|
-
case .operationNotAllowed: return "OPERATION_NOT_ALLOWED"
|
|
68
|
-
case .unknown: return "UNKNOWN"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
var message: String {
|
|
73
|
-
switch self {
|
|
74
|
-
case .notConnected(let m): return m
|
|
75
|
-
case .connectionLost(let m): return m
|
|
76
|
-
case .premiumRequired(let m): return m
|
|
77
|
-
case .invalidURI(let m): return m
|
|
78
|
-
case .invalidParameter(let m): return m
|
|
79
|
-
case .operationNotAllowed(let m): return m
|
|
80
|
-
case .unknown(let m): return m
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
final class PlayerException: Exception, @unchecked Sendable {
|
|
86
|
-
private let playerCode: String
|
|
87
|
-
private let playerMessage: String
|
|
88
|
-
|
|
89
|
-
init(_ error: NativePlayerError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
90
|
-
self.playerCode = error.code
|
|
91
|
-
self.playerMessage = error.message
|
|
92
|
-
super.init(file: file, line: line, function: function)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
override var code: String { playerCode }
|
|
96
|
-
override var reason: String { playerMessage }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// MARK: — User error types
|
|
100
|
-
|
|
101
|
-
enum NativeUserError: Error {
|
|
102
|
-
case notConnected(String)
|
|
103
|
-
case connectionLost(String)
|
|
104
|
-
case invalidURI(String)
|
|
105
|
-
case operationNotAllowed(String)
|
|
106
|
-
case unknown(String)
|
|
107
|
-
|
|
108
|
-
var code: String {
|
|
109
|
-
switch self {
|
|
110
|
-
case .notConnected: return "NOT_CONNECTED"
|
|
111
|
-
case .connectionLost: return "CONNECTION_LOST"
|
|
112
|
-
case .invalidURI: return "INVALID_URI"
|
|
113
|
-
case .operationNotAllowed: return "OPERATION_NOT_ALLOWED"
|
|
114
|
-
case .unknown: return "UNKNOWN"
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
var message: String {
|
|
119
|
-
switch self {
|
|
120
|
-
case .notConnected(let m): return m
|
|
121
|
-
case .connectionLost(let m): return m
|
|
122
|
-
case .invalidURI(let m): return m
|
|
123
|
-
case .operationNotAllowed(let m): return m
|
|
124
|
-
case .unknown(let m): return m
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
final class UserException: Exception, @unchecked Sendable {
|
|
130
|
-
private let userCode: String
|
|
131
|
-
private let userMessage: String
|
|
132
|
-
|
|
133
|
-
init(_ error: NativeUserError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
134
|
-
self.userCode = error.code
|
|
135
|
-
self.userMessage = error.message
|
|
136
|
-
super.init(file: file, line: line, function: function)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
override var code: String { userCode }
|
|
140
|
-
override var reason: String { userMessage }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// MARK: — Content error types
|
|
144
|
-
|
|
145
|
-
enum NativeContentError: Error {
|
|
146
|
-
case notConnected(String)
|
|
147
|
-
case connectionLost(String)
|
|
148
|
-
case contentAPIUnavailable(String)
|
|
149
|
-
case unknown(String)
|
|
150
|
-
|
|
151
|
-
var code: String {
|
|
152
|
-
switch self {
|
|
153
|
-
case .notConnected: return "NOT_CONNECTED"
|
|
154
|
-
case .connectionLost: return "CONNECTION_LOST"
|
|
155
|
-
case .contentAPIUnavailable: return "CONTENT_API_UNAVAILABLE"
|
|
156
|
-
case .unknown: return "UNKNOWN"
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
var message: String {
|
|
161
|
-
switch self {
|
|
162
|
-
case .notConnected(let m): return m
|
|
163
|
-
case .connectionLost(let m): return m
|
|
164
|
-
case .contentAPIUnavailable(let m): return m
|
|
165
|
-
case .unknown(let m): return m
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
final class ContentException: Exception, @unchecked Sendable {
|
|
171
|
-
private let contentCode: String
|
|
172
|
-
private let contentMessage: String
|
|
173
|
-
|
|
174
|
-
init(_ error: NativeContentError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
175
|
-
self.contentCode = error.code
|
|
176
|
-
self.contentMessage = error.message
|
|
177
|
-
super.init(file: file, line: line, function: function)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
override var code: String { contentCode }
|
|
181
|
-
override var reason: String { contentMessage }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// MARK: — Images error types
|
|
185
|
-
|
|
186
|
-
enum NativeImagesError: Error {
|
|
187
|
-
case notConnected(String)
|
|
188
|
-
case invalidURI(String)
|
|
189
|
-
case imageLoadFailed(String)
|
|
190
|
-
case unknown(String)
|
|
191
|
-
|
|
192
|
-
var code: String {
|
|
193
|
-
switch self {
|
|
194
|
-
case .notConnected: return "NOT_CONNECTED"
|
|
195
|
-
case .invalidURI: return "INVALID_URI"
|
|
196
|
-
case .imageLoadFailed: return "IMAGE_LOAD_FAILED"
|
|
197
|
-
case .unknown: return "UNKNOWN"
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
var message: String {
|
|
202
|
-
switch self {
|
|
203
|
-
case .notConnected(let m): return m
|
|
204
|
-
case .invalidURI(let m): return m
|
|
205
|
-
case .imageLoadFailed(let m): return m
|
|
206
|
-
case .unknown(let m): return m
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
final class ImagesException: Exception, @unchecked Sendable {
|
|
212
|
-
private let imagesCode: String
|
|
213
|
-
private let imagesMessage: String
|
|
214
|
-
|
|
215
|
-
init(_ error: NativeImagesError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
216
|
-
self.imagesCode = error.code
|
|
217
|
-
self.imagesMessage = error.message
|
|
218
|
-
super.init(file: file, line: line, function: function)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
override var code: String { imagesCode }
|
|
222
|
-
override var reason: String { imagesMessage }
|
|
223
|
-
}
|
|
224
|
-
|
|
225
5
|
// MARK: — Coordinator
|
|
226
6
|
|
|
227
7
|
/// Manages the `SPTAppRemote` singleton, the IPC connection lifecycle, and all
|
|
@@ -245,6 +25,14 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
245
25
|
private var appRemote: SPTAppRemote?
|
|
246
26
|
private var connectContinuation: CheckedContinuation<Void, Error>?
|
|
247
27
|
|
|
28
|
+
/// The `SPTAppRemote` awaiting an `authorizeAndPlay()` redirect, exposed
|
|
29
|
+
/// synchronously to the URL-redirect path. Mirrors the auth coordinator's
|
|
30
|
+
/// `nonisolated(unsafe)` `sessionManager`: it is only written from the
|
|
31
|
+
/// actor's executor (or the main-thread redirect handler) and read on the
|
|
32
|
+
/// main thread, so occasional cross-actor access is acceptable. `nil`
|
|
33
|
+
/// whenever no authorize-and-play flow is in flight.
|
|
34
|
+
nonisolated(unsafe) private var pendingAuthorizeRemote: SPTAppRemote?
|
|
35
|
+
|
|
248
36
|
nonisolated(unsafe) private(set) var connectionStateString: String = "disconnected"
|
|
249
37
|
|
|
250
38
|
var onConnectionStateChange: ((String) -> Void)?
|
|
@@ -266,9 +54,12 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
266
54
|
|
|
267
55
|
private init(sptConfiguration: SPTConfiguration) {
|
|
268
56
|
self.sptConfiguration = sptConfiguration
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
57
|
+
let connectionBridge = SpotifyAppRemoteDelegateBridge()
|
|
58
|
+
let playerStateBridge = SpotifyPlayerStateDelegateBridge()
|
|
59
|
+
let userCapabilitiesBridge = SpotifyUserCapabilitiesDelegateBridge()
|
|
60
|
+
self.connectionBridge = connectionBridge
|
|
61
|
+
self.playerStateBridge = playerStateBridge
|
|
62
|
+
self.userCapabilitiesBridge = userCapabilitiesBridge
|
|
272
63
|
connectionBridge.coordinator = self
|
|
273
64
|
playerStateBridge.coordinator = self
|
|
274
65
|
userCapabilitiesBridge.coordinator = self
|
|
@@ -314,6 +105,103 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
314
105
|
}
|
|
315
106
|
}
|
|
316
107
|
|
|
108
|
+
/// Wakes the Spotify app (via `authorizeAndPlayURI`), starts playback, and
|
|
109
|
+
/// then completes the App Remote connection once Spotify redirects back.
|
|
110
|
+
///
|
|
111
|
+
/// Unlike `connect()`, this works even when the Spotify app has been
|
|
112
|
+
/// suspended: `authorizeAndPlayURI` performs an app-switch to Spotify, which
|
|
113
|
+
/// revives it. Spotify then redirects back to the host app with an access
|
|
114
|
+
/// token; `handleAuthorizeRedirect(_:)` consumes that redirect and calls
|
|
115
|
+
/// `connect()` on the same `SPTAppRemote` instance.
|
|
116
|
+
///
|
|
117
|
+
/// The `connectContinuation` is reused for the whole flow, so `didConnect()`
|
|
118
|
+
/// / `didFailToConnect(error:)` resolve it exactly as for `connect()`.
|
|
119
|
+
func authorizeAndPlay(uri: String, accessToken: String) async throws {
|
|
120
|
+
if appRemote?.isConnected == true {
|
|
121
|
+
// Already connected — honour the request by (re)starting playback.
|
|
122
|
+
if uri.isEmpty {
|
|
123
|
+
try await playerResume()
|
|
124
|
+
} else {
|
|
125
|
+
try await playerPlay(uri: uri)
|
|
126
|
+
}
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
guard connectContinuation == nil else {
|
|
130
|
+
throw AppRemoteError.connectionFailed("A connection attempt is already in progress")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
transitionState("connecting")
|
|
134
|
+
|
|
135
|
+
let params = SPTAppRemoteConnectionParams(
|
|
136
|
+
accessToken: accessToken,
|
|
137
|
+
defaultImageSize: CGSize.zero,
|
|
138
|
+
imageFormat: .any
|
|
139
|
+
)
|
|
140
|
+
let remote = SPTAppRemote(
|
|
141
|
+
configuration: sptConfiguration,
|
|
142
|
+
connectionParameters: params,
|
|
143
|
+
logLevel: .error
|
|
144
|
+
)
|
|
145
|
+
remote.delegate = connectionBridge
|
|
146
|
+
appRemote = remote
|
|
147
|
+
pendingAuthorizeRemote = remote
|
|
148
|
+
|
|
149
|
+
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
|
150
|
+
self.connectContinuation = cont
|
|
151
|
+
Task { @MainActor in
|
|
152
|
+
// `authorizeAndPlayURI` app-switches to Spotify to wake it. `success`
|
|
153
|
+
// is false when the Spotify app could not be opened (e.g. not
|
|
154
|
+
// installed) — no redirect will arrive, so fail the continuation now.
|
|
155
|
+
// On success we await the redirect, handled by
|
|
156
|
+
// `handleAuthorizeRedirect(_:)`, which completes the connection.
|
|
157
|
+
let success = await remote.authorizeAndPlayURI(uri)
|
|
158
|
+
if !success {
|
|
159
|
+
await self.didFailToConnect(error: NSError(
|
|
160
|
+
domain: "ExpoSpotifySDK",
|
|
161
|
+
code: -1,
|
|
162
|
+
userInfo: [NSLocalizedDescriptionKey: "authorizeAndPlay: could not open the Spotify app (is it installed?)"]
|
|
163
|
+
))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Consumes a redirect URL produced by an `authorizeAndPlay()` app-switch.
|
|
170
|
+
/// Returns `true` if an authorize flow was pending and the URL was handled
|
|
171
|
+
/// (so the AppDelegate should not also forward it to the auth coordinator).
|
|
172
|
+
///
|
|
173
|
+
/// Synchronous + `nonisolated` because the AppDelegate `open url` hook
|
|
174
|
+
/// expects an immediate `Bool`, matching `SpotifyAuthCoordinator.handleOpenURL`.
|
|
175
|
+
nonisolated func handleAuthorizeRedirect(_ url: URL) -> Bool {
|
|
176
|
+
guard let remote = pendingAuthorizeRemote else { return false }
|
|
177
|
+
let consume: () -> Bool = { [self] in
|
|
178
|
+
MainActor.assumeIsolated {
|
|
179
|
+
let params = remote.authorizationParameters(from: url)
|
|
180
|
+
if let token = params?[SPTAppRemoteAccessTokenKey] {
|
|
181
|
+
remote.connectionParameters.accessToken = token
|
|
182
|
+
pendingAuthorizeRemote = nil
|
|
183
|
+
remote.connect()
|
|
184
|
+
return true
|
|
185
|
+
} else if let errorDescription = params?[SPTAppRemoteErrorDescriptionKey] {
|
|
186
|
+
pendingAuthorizeRemote = nil
|
|
187
|
+
Task {
|
|
188
|
+
await self.didFailToConnect(error: NSError(
|
|
189
|
+
domain: "ExpoSpotifySDK",
|
|
190
|
+
code: -2,
|
|
191
|
+
userInfo: [NSLocalizedDescriptionKey: errorDescription]
|
|
192
|
+
))
|
|
193
|
+
}
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if Thread.isMainThread {
|
|
200
|
+
return consume()
|
|
201
|
+
}
|
|
202
|
+
return DispatchQueue.main.sync(execute: consume)
|
|
203
|
+
}
|
|
204
|
+
|
|
317
205
|
func disconnect() {
|
|
318
206
|
if let cont = connectContinuation {
|
|
319
207
|
connectContinuation = nil
|
|
@@ -323,6 +211,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
323
211
|
teardownCapabilitiesSubscription()
|
|
324
212
|
appRemote?.disconnect()
|
|
325
213
|
appRemote = nil
|
|
214
|
+
pendingAuthorizeRemote = nil
|
|
326
215
|
transitionState("disconnected")
|
|
327
216
|
}
|
|
328
217
|
|
|
@@ -337,6 +226,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
337
226
|
// MARK: — Connection delegate callbacks
|
|
338
227
|
|
|
339
228
|
func didConnect() {
|
|
229
|
+
pendingAuthorizeRemote = nil
|
|
340
230
|
transitionState("connected")
|
|
341
231
|
setupPlayerSubscription()
|
|
342
232
|
setupCapabilitiesSubscription()
|
|
@@ -347,6 +237,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
347
237
|
|
|
348
238
|
func didFailToConnect(error: Error?) {
|
|
349
239
|
appRemote = nil
|
|
240
|
+
pendingAuthorizeRemote = nil
|
|
350
241
|
let msg = error.map { describeNSError($0 as NSError) } ?? "Unknown connection failure"
|
|
351
242
|
let remoteError = AppRemoteError.connectionFailed(msg)
|
|
352
243
|
transitionState("disconnected")
|
|
@@ -381,7 +272,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
381
272
|
|
|
382
273
|
/// Called by `SpotifyPlayerStateDelegateBridge` when a state update arrives.
|
|
383
274
|
func playerStateDidChange(_ state: any SPTAppRemotePlayerState) {
|
|
384
|
-
onPlayerStateChange?(
|
|
275
|
+
onPlayerStateChange?(SpotifyAppRemoteMappers.playerStateToMap(state))
|
|
385
276
|
}
|
|
386
277
|
|
|
387
278
|
// MARK: — User subscription
|
|
@@ -400,7 +291,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
400
291
|
|
|
401
292
|
/// Called by `SpotifyUserCapabilitiesDelegateBridge` on each capabilities update.
|
|
402
293
|
func userCapabilitiesDidChange(_ capabilities: any SPTAppRemoteUserCapabilities) {
|
|
403
|
-
onCapabilitiesChange?(
|
|
294
|
+
onCapabilitiesChange?(SpotifyAppRemoteMappers.capabilitiesToMap(capabilities))
|
|
404
295
|
}
|
|
405
296
|
|
|
406
297
|
// MARK: — Player transport
|
|
@@ -458,9 +349,9 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
458
349
|
return try await withCheckedThrowingContinuation { cont in
|
|
459
350
|
playerAPI.getPlayerState { result, error in
|
|
460
351
|
if let error = error {
|
|
461
|
-
cont.resume(throwing:
|
|
352
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: "Player.getPlayerState"))
|
|
462
353
|
} else if let state = result as? any SPTAppRemotePlayerState {
|
|
463
|
-
cont.resume(returning:
|
|
354
|
+
cont.resume(returning: SpotifyAppRemoteMappers.playerStateToMap(state))
|
|
464
355
|
} else {
|
|
465
356
|
cont.resume(throwing: NativePlayerError.unknown("Player.getPlayerState: unexpected result type"))
|
|
466
357
|
}
|
|
@@ -473,7 +364,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
473
364
|
return try await withCheckedThrowingContinuation { cont in
|
|
474
365
|
playerAPI.getCrossfadeState { result, error in
|
|
475
366
|
if let error = error {
|
|
476
|
-
cont.resume(throwing:
|
|
367
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: "Player.getCrossfadeState"))
|
|
477
368
|
} else if let state = result as? any SPTAppRemoteCrossfadeState {
|
|
478
369
|
cont.resume(returning: ["isEnabled": state.isEnabled, "duration": state.duration])
|
|
479
370
|
} else {
|
|
@@ -489,7 +380,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
489
380
|
let speeds: [any SPTAppRemotePodcastPlaybackSpeed] = try await withCheckedThrowingContinuation { cont in
|
|
490
381
|
playerAPI.getAvailablePodcastPlaybackSpeeds { result, error in
|
|
491
382
|
if let error = error {
|
|
492
|
-
cont.resume(throwing:
|
|
383
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: "Player.setPodcastPlaybackSpeed"))
|
|
493
384
|
} else if let speeds = result as? [any SPTAppRemotePodcastPlaybackSpeed] {
|
|
494
385
|
cont.resume(returning: speeds)
|
|
495
386
|
} else {
|
|
@@ -515,9 +406,9 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
515
406
|
return try await withCheckedThrowingContinuation { cont in
|
|
516
407
|
userAPI.fetchCapabilities { result, error in
|
|
517
408
|
if let error = error {
|
|
518
|
-
cont.resume(throwing:
|
|
409
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.getCapabilities"))
|
|
519
410
|
} else if let capabilities = result as? any SPTAppRemoteUserCapabilities {
|
|
520
|
-
cont.resume(returning:
|
|
411
|
+
cont.resume(returning: SpotifyAppRemoteMappers.capabilitiesToMap(capabilities))
|
|
521
412
|
} else {
|
|
522
413
|
cont.resume(throwing: NativeUserError.unknown("User.getCapabilities: unexpected result type"))
|
|
523
414
|
}
|
|
@@ -530,9 +421,9 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
530
421
|
return try await withCheckedThrowingContinuation { cont in
|
|
531
422
|
userAPI.fetchLibraryState(forURI: uri) { result, error in
|
|
532
423
|
if let error = error {
|
|
533
|
-
cont.resume(throwing:
|
|
424
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.getLibraryState"))
|
|
534
425
|
} else if let state = result as? any SPTAppRemoteLibraryState {
|
|
535
|
-
cont.resume(returning:
|
|
426
|
+
cont.resume(returning: SpotifyAppRemoteMappers.libraryStateToMap(state))
|
|
536
427
|
} else {
|
|
537
428
|
cont.resume(throwing: NativeUserError.unknown("User.getLibraryState: unexpected result type"))
|
|
538
429
|
}
|
|
@@ -545,9 +436,9 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
545
436
|
return try await withCheckedThrowingContinuation { cont in
|
|
546
437
|
userAPI.addItemToLibrary(withURI: uri) { result, error in
|
|
547
438
|
if let error = error {
|
|
548
|
-
cont.resume(throwing:
|
|
439
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.addToLibrary"))
|
|
549
440
|
} else if let state = result as? any SPTAppRemoteLibraryState {
|
|
550
|
-
cont.resume(returning:
|
|
441
|
+
cont.resume(returning: SpotifyAppRemoteMappers.libraryStateToMap(state))
|
|
551
442
|
} else {
|
|
552
443
|
cont.resume(throwing: NativeUserError.unknown("User.addToLibrary: unexpected result type"))
|
|
553
444
|
}
|
|
@@ -560,9 +451,9 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
560
451
|
return try await withCheckedThrowingContinuation { cont in
|
|
561
452
|
userAPI.removeItemFromLibrary(withURI: uri) { result, error in
|
|
562
453
|
if let error = error {
|
|
563
|
-
cont.resume(throwing:
|
|
454
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.removeFromLibrary"))
|
|
564
455
|
} else if let state = result as? any SPTAppRemoteLibraryState {
|
|
565
|
-
cont.resume(returning:
|
|
456
|
+
cont.resume(returning: SpotifyAppRemoteMappers.libraryStateToMap(state))
|
|
566
457
|
} else {
|
|
567
458
|
cont.resume(throwing: NativeUserError.unknown("User.removeFromLibrary: unexpected result type"))
|
|
568
459
|
}
|
|
@@ -575,11 +466,11 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
575
466
|
func contentGetRecommendedContentItems(type: String) async throws -> [[String: Any]] {
|
|
576
467
|
let contentAPI = try requireContentAPI(callsite: "Content.getRecommendedContentItems")
|
|
577
468
|
return try await withCheckedThrowingContinuation { cont in
|
|
578
|
-
contentAPI.fetchRecommendedContentItems(forType:
|
|
469
|
+
contentAPI.fetchRecommendedContentItems(forType: SpotifyAppRemoteMappers.mapContentType(type), flattenContainers: false) { result, error in
|
|
579
470
|
if let error = error {
|
|
580
|
-
cont.resume(throwing:
|
|
471
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapContentError(error as NSError, callsite: "Content.getRecommendedContentItems"))
|
|
581
472
|
} else if let list = result as? [any SPTAppRemoteContentItem] {
|
|
582
|
-
cont.resume(returning: list.map(
|
|
473
|
+
cont.resume(returning: list.map(SpotifyAppRemoteMappers.contentItemToMap))
|
|
583
474
|
} else {
|
|
584
475
|
cont.resume(throwing: NativeContentError.unknown("Content.getRecommendedContentItems: unexpected result type"))
|
|
585
476
|
}
|
|
@@ -596,7 +487,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
596
487
|
let contentItem: any SPTAppRemoteContentItem = try await withCheckedThrowingContinuation { cont in
|
|
597
488
|
contentAPI.fetchContentItem(forURI: uri) { result, error in
|
|
598
489
|
if let error = error {
|
|
599
|
-
cont.resume(throwing:
|
|
490
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapContentError(error as NSError, callsite: "Content.getChildren"))
|
|
600
491
|
} else if let item = result as? any SPTAppRemoteContentItem {
|
|
601
492
|
cont.resume(returning: item)
|
|
602
493
|
} else {
|
|
@@ -608,9 +499,9 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
608
499
|
return try await withCheckedThrowingContinuation { cont in
|
|
609
500
|
contentAPI.fetchChildren(of: contentItem, callback: { result, error in
|
|
610
501
|
if let error = error {
|
|
611
|
-
cont.resume(throwing:
|
|
502
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapContentError(error as NSError, callsite: "Content.getChildren"))
|
|
612
503
|
} else if let children = result as? [any SPTAppRemoteContentItem] {
|
|
613
|
-
cont.resume(returning: children.map(
|
|
504
|
+
cont.resume(returning: children.map(SpotifyAppRemoteMappers.contentItemToMap))
|
|
614
505
|
} else {
|
|
615
506
|
cont.resume(throwing: NativeContentError.unknown("Content.getChildren: unexpected result type"))
|
|
616
507
|
}
|
|
@@ -626,12 +517,12 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
626
517
|
}
|
|
627
518
|
let imageAPI = try requireImageAPI(callsite: "Images.load")
|
|
628
519
|
let representable = LocalImageRepresentable(imageIdentifier: imageIdentifier)
|
|
629
|
-
let targetSize =
|
|
520
|
+
let targetSize = SpotifyAppRemoteMappers.mapImageSize(size)
|
|
630
521
|
|
|
631
522
|
let image: UIImage = try await withCheckedThrowingContinuation { cont in
|
|
632
523
|
imageAPI.fetchImage(forItem: representable, with: targetSize) { result, error in
|
|
633
524
|
if let error = error {
|
|
634
|
-
cont.resume(throwing:
|
|
525
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapImagesError(error as NSError, callsite: "Images.load"))
|
|
635
526
|
} else if let image = result as? UIImage {
|
|
636
527
|
cont.resume(returning: image)
|
|
637
528
|
} else {
|
|
@@ -699,7 +590,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
699
590
|
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
|
700
591
|
block { _, error in
|
|
701
592
|
if let error = error {
|
|
702
|
-
cont.resume(throwing:
|
|
593
|
+
cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: callsite))
|
|
703
594
|
} else {
|
|
704
595
|
cont.resume()
|
|
705
596
|
}
|
|
@@ -707,164 +598,6 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
707
598
|
}
|
|
708
599
|
}
|
|
709
600
|
|
|
710
|
-
private static func normalizePlayerError(_ error: NSError, callsite: String) -> NativePlayerError {
|
|
711
|
-
guard error.domain == SPTAppRemoteErrorDomain else {
|
|
712
|
-
return .unknown(error.localizedDescription)
|
|
713
|
-
}
|
|
714
|
-
switch error.code {
|
|
715
|
-
case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
|
|
716
|
-
return .connectionLost("\(callsite): connection to Spotify app was terminated")
|
|
717
|
-
case SPTAppRemoteErrorCode.invalidArgumentsError.rawValue:
|
|
718
|
-
return .invalidParameter(error.localizedDescription)
|
|
719
|
-
case SPTAppRemoteErrorCode.requestFailedError.rawValue:
|
|
720
|
-
let desc = error.localizedDescription.lowercased()
|
|
721
|
-
if desc.contains("premium") {
|
|
722
|
-
return .premiumRequired("\(callsite): Spotify Premium is required for on-demand playback")
|
|
723
|
-
}
|
|
724
|
-
if desc.contains("not allowed") || desc.contains("restriction") {
|
|
725
|
-
return .operationNotAllowed("\(callsite): \(error.localizedDescription)")
|
|
726
|
-
}
|
|
727
|
-
return .unknown(error.localizedDescription)
|
|
728
|
-
default:
|
|
729
|
-
return .unknown(error.localizedDescription)
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
private static func playerStateToMap(_ state: any SPTAppRemotePlayerState) -> [String: Any] {
|
|
734
|
-
let track = state.track
|
|
735
|
-
let restrictions = state.playbackRestrictions
|
|
736
|
-
let options = state.playbackOptions
|
|
737
|
-
return [
|
|
738
|
-
"track": [
|
|
739
|
-
"uri": track.uri,
|
|
740
|
-
"name": track.name,
|
|
741
|
-
"imageIdentifier": track.imageIdentifier,
|
|
742
|
-
"duration": track.duration,
|
|
743
|
-
"artist": ["name": track.artist.name, "uri": track.artist.uri],
|
|
744
|
-
"album": ["name": track.album.name, "uri": track.album.uri],
|
|
745
|
-
"isSaved": track.isSaved,
|
|
746
|
-
"isEpisode": track.isEpisode,
|
|
747
|
-
"isPodcast": track.isPodcast,
|
|
748
|
-
"isAdvertisement": track.isAdvertisement,
|
|
749
|
-
] as [String: Any],
|
|
750
|
-
"playbackPosition": state.playbackPosition,
|
|
751
|
-
"playbackSpeed": state.playbackSpeed,
|
|
752
|
-
"isPaused": state.isPaused,
|
|
753
|
-
"playbackOptions": [
|
|
754
|
-
"isShuffling": options.isShuffling,
|
|
755
|
-
"repeatMode": options.repeatMode.rawValue,
|
|
756
|
-
] as [String: Any],
|
|
757
|
-
"playbackRestrictions": [
|
|
758
|
-
"canSkipNext": restrictions.canSkipNext,
|
|
759
|
-
"canSkipPrevious": restrictions.canSkipPrevious,
|
|
760
|
-
"canRepeatTrack": restrictions.canRepeatTrack,
|
|
761
|
-
"canRepeatContext": restrictions.canRepeatContext,
|
|
762
|
-
"canToggleShuffle": restrictions.canToggleShuffle,
|
|
763
|
-
"canSeek": restrictions.canSeek,
|
|
764
|
-
] as [String: Any],
|
|
765
|
-
"contextTitle": state.contextTitle,
|
|
766
|
-
"contextUri": state.contextURI.absoluteString,
|
|
767
|
-
]
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
private static func capabilitiesToMap(_ capabilities: any SPTAppRemoteUserCapabilities) -> [String: Any] {
|
|
771
|
-
["canPlayOnDemand": capabilities.canPlayOnDemand]
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
private static func libraryStateToMap(_ state: any SPTAppRemoteLibraryState) -> [String: Any] {
|
|
775
|
-
["uri": state.uri, "isAdded": state.isAdded, "canAdd": state.canAdd]
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
private static func normalizeUserError(_ error: NSError, callsite: String) -> NativeUserError {
|
|
779
|
-
guard error.domain == SPTAppRemoteErrorDomain else {
|
|
780
|
-
return .unknown(error.localizedDescription)
|
|
781
|
-
}
|
|
782
|
-
switch error.code {
|
|
783
|
-
case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
|
|
784
|
-
return .connectionLost("\(callsite): connection to Spotify app was terminated")
|
|
785
|
-
case SPTAppRemoteErrorCode.invalidArgumentsError.rawValue:
|
|
786
|
-
return .invalidURI("\(callsite): \(error.localizedDescription)")
|
|
787
|
-
case SPTAppRemoteErrorCode.requestFailedError.rawValue:
|
|
788
|
-
let desc = error.localizedDescription.lowercased()
|
|
789
|
-
if desc.contains("not allowed") || desc.contains("restriction") {
|
|
790
|
-
return .operationNotAllowed("\(callsite): \(error.localizedDescription)")
|
|
791
|
-
}
|
|
792
|
-
return .unknown(error.localizedDescription)
|
|
793
|
-
default:
|
|
794
|
-
return .unknown(error.localizedDescription)
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
private static func normalizeContentError(_ error: NSError, callsite: String) -> NativeContentError {
|
|
799
|
-
guard error.domain == SPTAppRemoteErrorDomain else {
|
|
800
|
-
return .unknown(error.localizedDescription)
|
|
801
|
-
}
|
|
802
|
-
switch error.code {
|
|
803
|
-
case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
|
|
804
|
-
return .connectionLost("\(callsite): connection to Spotify app was terminated")
|
|
805
|
-
case SPTAppRemoteErrorCode.requestFailedError.rawValue:
|
|
806
|
-
let desc = error.localizedDescription.lowercased()
|
|
807
|
-
if desc.contains("not supported") || desc.contains("unsupported") {
|
|
808
|
-
return .contentAPIUnavailable("\(callsite): content API is unavailable on this Spotify app version")
|
|
809
|
-
}
|
|
810
|
-
return .unknown(error.localizedDescription)
|
|
811
|
-
default:
|
|
812
|
-
return .unknown(error.localizedDescription)
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
private static func normalizeImagesError(_ error: NSError, callsite: String) -> NativeImagesError {
|
|
817
|
-
guard error.domain == SPTAppRemoteErrorDomain else {
|
|
818
|
-
return .unknown(error.localizedDescription)
|
|
819
|
-
}
|
|
820
|
-
switch error.code {
|
|
821
|
-
case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
|
|
822
|
-
return .notConnected("\(callsite): connection to Spotify app was terminated")
|
|
823
|
-
case SPTAppRemoteErrorCode.invalidArgumentsError.rawValue:
|
|
824
|
-
return .invalidURI("\(callsite): invalid image identifier")
|
|
825
|
-
case SPTAppRemoteErrorCode.requestFailedError.rawValue:
|
|
826
|
-
return .imageLoadFailed("\(callsite): Spotify rejected image request")
|
|
827
|
-
default:
|
|
828
|
-
return .unknown(error.localizedDescription)
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
private static func mapContentType(_ type: String) -> String {
|
|
833
|
-
switch type {
|
|
834
|
-
case "navigation": return SPTAppRemoteContentTypeNavigation
|
|
835
|
-
case "fitness": return SPTAppRemoteContentTypeFitness
|
|
836
|
-
case "gaming": return SPTAppRemoteContentTypeGaming
|
|
837
|
-
default: return SPTAppRemoteContentTypeDefault
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
private static func mapImageSize(_ size: String) -> CGSize {
|
|
842
|
-
switch size {
|
|
843
|
-
case "small": return CGSize(width: 64, height: 64)
|
|
844
|
-
case "medium": return CGSize(width: 300, height: 300)
|
|
845
|
-
default: return CGSize(width: 640, height: 640)
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
private static func contentItemToMap(_ item: any SPTAppRemoteContentItem) -> [String: Any] {
|
|
850
|
-
var map: [String: Any] = [
|
|
851
|
-
"title": item.title as Any,
|
|
852
|
-
"subtitle": item.subtitle as Any,
|
|
853
|
-
"contentDescription": item.contentDescription as Any,
|
|
854
|
-
"identifier": item.identifier,
|
|
855
|
-
"uri": item.uri,
|
|
856
|
-
"imageIdentifier": item.imageIdentifier,
|
|
857
|
-
"isAvailableOffline": item.isAvailableOffline,
|
|
858
|
-
"isPlayable": item.isPlayable,
|
|
859
|
-
"isContainer": item.isContainer,
|
|
860
|
-
"isPinned": item.isPinned,
|
|
861
|
-
]
|
|
862
|
-
if let children = item.children {
|
|
863
|
-
map["children"] = children.map(Self.contentItemToMap)
|
|
864
|
-
}
|
|
865
|
-
return map
|
|
866
|
-
}
|
|
867
|
-
|
|
868
601
|
private func transitionState(_ state: String) {
|
|
869
602
|
connectionStateString = state
|
|
870
603
|
onConnectionStateChange?(state)
|