You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
416 lines
22 KiB
416 lines
22 KiB
import Flutter
|
|
import SpotifyiOS
|
|
|
|
public class SwiftSpotifySdkPlugin: NSObject, FlutterPlugin {
|
|
private static var instance = SwiftSpotifySdkPlugin()
|
|
private var appRemote: SPTAppRemote?
|
|
private var connectionStatusHandler: ConnectionStatusHandler?
|
|
private var playerStateHandler: PlayerStateHandler?
|
|
private var playerContextHandler: PlayerContextHandler?
|
|
private static var playerStateChannel: FlutterEventChannel?
|
|
private static var playerContextChannel: FlutterEventChannel?
|
|
|
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
guard playerStateChannel == nil else {
|
|
// Avoid multiple plugin registations
|
|
return
|
|
}
|
|
let spotifySDKChannel = FlutterMethodChannel(name: "spotify_sdk", binaryMessenger: registrar.messenger())
|
|
let connectionStatusChannel = FlutterEventChannel(name: "connection_status_subscription", binaryMessenger: registrar.messenger())
|
|
playerStateChannel = FlutterEventChannel(name: "player_state_subscription", binaryMessenger: registrar.messenger())
|
|
playerContextChannel = FlutterEventChannel(name: "player_context_subscription", binaryMessenger: registrar.messenger())
|
|
registrar.addApplicationDelegate(instance)
|
|
registrar.addMethodCallDelegate(instance, channel: spotifySDKChannel)
|
|
instance.connectionStatusHandler = ConnectionStatusHandler()
|
|
connectionStatusChannel.setStreamHandler(instance.connectionStatusHandler)
|
|
}
|
|
|
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
var defaultPlayAPICallback: SPTAppRemoteCallback {
|
|
get {
|
|
return {_, error in
|
|
if let error = error {
|
|
result(FlutterError(code: "PlayerAPI Error", message: error.localizedDescription, details: nil))
|
|
} else {
|
|
result(true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
switch call.method {
|
|
case SpotifySdkConstants.methodConnectToSpotify:
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let clientID = swiftArguments[SpotifySdkConstants.paramClientId] as? String,
|
|
!clientID.isEmpty else {
|
|
result(FlutterError(code: "Argument Error", message: "Client ID is not set", details: nil))
|
|
return
|
|
}
|
|
|
|
guard let url = swiftArguments[SpotifySdkConstants.paramRedirectUrl] as? String,
|
|
!url.isEmpty else {
|
|
result(FlutterError(code: "Argument Error", message: "Redirect URL is not set", details: nil))
|
|
return
|
|
}
|
|
|
|
connectionStatusHandler?.connectionResult = result
|
|
|
|
|
|
let accessToken: String? = swiftArguments[SpotifySdkConstants.paramAccessToken] as? String
|
|
let spotifyUri: String = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String ?? ""
|
|
|
|
do {
|
|
try connectToSpotify(clientId: clientID, redirectURL: url, accessToken: accessToken, spotifyUri: spotifyUri, asRadio: swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool, additionalScopes: swiftArguments[SpotifySdkConstants.scope] as? String)
|
|
}
|
|
catch SpotifyError.redirectURLInvalid {
|
|
result(FlutterError(code: "errorConnecting", message: "Redirect URL is not set or has invalid format", details: nil))
|
|
}
|
|
catch {
|
|
result(FlutterError(code: "CouldNotFindSpotifyApp", message: "The Spotify app is not installed on the device", details: nil))
|
|
return
|
|
}
|
|
|
|
case SpotifySdkConstants.methodGetAccessToken:
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let clientID = swiftArguments[SpotifySdkConstants.paramClientId] as? String,
|
|
let url = swiftArguments[SpotifySdkConstants.paramRedirectUrl] as? String else {
|
|
result(FlutterError(code: "Arguments Error", message: "One or more arguments are missing", details: nil))
|
|
return
|
|
}
|
|
connectionStatusHandler?.tokenResult = result
|
|
let spotifyUri: String = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String ?? ""
|
|
|
|
do {
|
|
try connectToSpotify(clientId: clientID, redirectURL: url, spotifyUri: spotifyUri, asRadio: swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool, additionalScopes: swiftArguments[SpotifySdkConstants.scope] as? String)
|
|
}
|
|
catch SpotifyError.redirectURLInvalid {
|
|
result(FlutterError(code: "errorConnecting", message: "Redirect URL is not set or has invalid format", details: nil))
|
|
}
|
|
catch {
|
|
result(FlutterError(code: "CouldNotFindSpotifyApp", message: "The Spotify app is not installed on the device", details: nil))
|
|
return
|
|
}
|
|
case SpotifySdkConstants.methodGetImage:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let paramImageUri = swiftArguments[SpotifySdkConstants.paramImageUri] as? String,
|
|
let paramImageDimension = swiftArguments[SpotifySdkConstants.paramImageDimension] as? Int else {
|
|
result(FlutterError(code: "Arguments Error", message: "One or more arguments are missing", details: nil))
|
|
return
|
|
}
|
|
|
|
class ImageObject: NSObject, SPTAppRemoteImageRepresentable {
|
|
var imageIdentifier: String = ""
|
|
}
|
|
|
|
let imageObject = ImageObject()
|
|
imageObject.imageIdentifier = paramImageUri
|
|
appRemote.imageAPI?.fetchImage(forItem: imageObject, with: CGSize(width: paramImageDimension, height: paramImageDimension), callback: { (image, error) in
|
|
guard error == nil else {
|
|
result(FlutterError(code: "ImageAPI Error", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
guard let imageData = (image as? UIImage)?.pngData() else {
|
|
result(FlutterError(code: "ImageAPI Error", message: "Image is empty", details: nil))
|
|
return
|
|
}
|
|
result(imageData)
|
|
})
|
|
case SpotifySdkConstants.methodGetPlayerState:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
|
|
appRemote.playerAPI?.getPlayerState({ (playerState, error) in
|
|
guard error == nil else {
|
|
result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
guard let playerState = playerState as? SPTAppRemotePlayerState else {
|
|
result(FlutterError(code: "PlayerAPI Error", message: "PlayerState is empty", details: nil))
|
|
return
|
|
}
|
|
result(State.playerStateDictionary(playerState).json)
|
|
})
|
|
case SpotifySdkConstants.methodDisconnectFromSpotify:
|
|
appRemote?.disconnect()
|
|
// appRemote?.connectionParameters.accessToken = nil
|
|
result(true)
|
|
case SpotifySdkConstants.methodPlay:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
|
|
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
|
|
return
|
|
}
|
|
let asRadio: Bool = (swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool) ?? false
|
|
appRemote.playerAPI?.play(uri, asRadio: asRadio, callback: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodPause:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.pause(defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodResume:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.resume(defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodSkipNext:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.skip(toNext: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodSkipPrevious:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.skip(toPrevious: { (spotifyResult, error) in
|
|
if let error = error {
|
|
result(FlutterError(code: "PlayerAPI Error", message: error.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
result(true)
|
|
})
|
|
case SpotifySdkConstants.methodSkipToIndex:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
|
|
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
|
|
return
|
|
}
|
|
let index = (swiftArguments[SpotifySdkConstants.paramTrackIndex] as? Int) ?? 0
|
|
|
|
appRemote.contentAPI?.fetchContentItem(forURI: uri, callback: { (contentItemResult, error) in
|
|
guard error == nil else {
|
|
result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
guard let contentItem = contentItemResult as? SPTAppRemoteContentItem else {
|
|
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.play(contentItem, skipToTrackIndex: index, callback: defaultPlayAPICallback)
|
|
})
|
|
|
|
case SpotifySdkConstants.methodAddToLibrary:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
|
|
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.userAPI?.addItemToLibrary(withURI: uri, callback: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodRemoveFromLibrary:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
|
|
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.userAPI?.removeItemFromLibrary(withURI: uri, callback: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodGetCapabilities:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
appRemote.userAPI?.fetchCapabilities(callback: { (capabilitiesResult, error) in
|
|
guard error == nil else {
|
|
result(FlutterError(code: "getCapabilitiesError", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
guard let userCapabilities = capabilitiesResult as? SPTAppRemoteUserCapabilities else {
|
|
result(FlutterError(code: "getCapabilitiesError", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
|
|
result(State.userCapabilitiesDictionary(userCapabilities).json)
|
|
})
|
|
case SpotifySdkConstants.methodQueueTrack:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
|
|
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.enqueueTrackUri(uri, callback: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodSeekTo:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let position = swiftArguments[SpotifySdkConstants.paramPositionedMilliseconds] as? Int else {
|
|
result(FlutterError(code: "Position error", message: "No position was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.seek(toPosition: position, callback: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodGetCrossfadeState:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.getCrossfadeState({ (crossfadeState, error) in
|
|
guard error == nil else {
|
|
result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
guard let crossfadeState = crossfadeState as? SPTAppRemoteCrossfadeState else {
|
|
result(FlutterError(code: "PlayerAPI Error", message: "PlayerState is empty", details: nil))
|
|
return
|
|
}
|
|
result(State.crossfadeStateDictionary(crossfadeState).json)
|
|
})
|
|
case SpotifySdkConstants.methodSetShuffle:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let shuffle = swiftArguments[SpotifySdkConstants.paramShuffle] as? Bool else {
|
|
result(FlutterError(code: "Shuffle mode error", message: "No ShuffleMode was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.setShuffle(shuffle, callback: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.methodSetRepeatMode:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let repeatModeIndex = swiftArguments[SpotifySdkConstants.paramRepeatMode] as? UInt,
|
|
let repeatMode = SPTAppRemotePlaybackOptionsRepeatMode(rawValue: repeatModeIndex) else {
|
|
result(FlutterError(code: "Repeat mode error", message: "No RepeatMode was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.playerAPI?.setRepeatMode(repeatMode, callback: defaultPlayAPICallback)
|
|
case SpotifySdkConstants.getLibraryState:
|
|
guard let appRemote = appRemote else {
|
|
result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
|
|
return
|
|
}
|
|
guard let swiftArguments = call.arguments as? [String:Any],
|
|
let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
|
|
result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
|
|
return
|
|
}
|
|
appRemote.userAPI?.fetchLibraryState(forURI: uri, callback: {libraryStateResult, error in
|
|
guard error == nil else {
|
|
result(FlutterError(code: "fetchLibraryStateError", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
guard let libraryState = libraryStateResult as? SPTAppRemoteLibraryState else {
|
|
result(FlutterError(code: "fetchLibraryStateError", message: error?.localizedDescription, details: nil))
|
|
return
|
|
}
|
|
|
|
result(State.libraryStateDictionary(libraryState).json)
|
|
})
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
|
|
private func connectToSpotify(clientId: String, redirectURL: String, accessToken: String? = nil, spotifyUri: String = "", asRadio: Bool?, additionalScopes: String? = nil) throws {
|
|
func configureAppRemote(clientID: String, redirectURL: String, accessToken: String? = nil) throws {
|
|
guard let redirectURL = URL(string: redirectURL) else {
|
|
throw SpotifyError.redirectURLInvalid
|
|
}
|
|
let configuration = SPTConfiguration(clientID: clientID, redirectURL: redirectURL)
|
|
let appRemote = SPTAppRemote(configuration: configuration, logLevel: .none)
|
|
appRemote.delegate = connectionStatusHandler
|
|
let playerDelegate = PlayerDelegate()
|
|
playerStateHandler = PlayerStateHandler(appRemote: appRemote, playerDelegate: playerDelegate)
|
|
SwiftSpotifySdkPlugin.playerStateChannel?.setStreamHandler(playerStateHandler)
|
|
|
|
playerContextHandler = PlayerContextHandler(appRemote: appRemote, playerDelegate: playerDelegate)
|
|
SwiftSpotifySdkPlugin.playerContextChannel?.setStreamHandler(playerContextHandler)
|
|
|
|
appRemote.connectionParameters.accessToken = accessToken
|
|
self.appRemote = appRemote
|
|
}
|
|
|
|
try configureAppRemote(clientID: clientId, redirectURL: redirectURL, accessToken: accessToken)
|
|
|
|
var scopes: [String]?
|
|
if let additionalScopes = additionalScopes {
|
|
scopes = additionalScopes.components(separatedBy: ",")
|
|
}
|
|
|
|
if accessToken != nil {
|
|
appRemote?.connect()
|
|
} else {
|
|
// Note: A blank string will play the user's last song or pick a random one.
|
|
self.appRemote?.authorizeAndPlayURI(spotifyUri, asRadio: asRadio ?? false, additionalScopes: scopes) { success in
|
|
if (!success) {
|
|
self.connectionStatusHandler?.connectionResult?(FlutterError(code: "spotifyNotInstalled", message: "Spotify app is not installed", details: nil))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SwiftSpotifySdkPlugin {
|
|
public func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
|
setAccessTokenFromURL(url: url)
|
|
return true
|
|
}
|
|
|
|
public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]) -> Void) -> Bool {
|
|
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
|
let url = userActivity.webpageURL
|
|
else {
|
|
connectionStatusHandler?.connectionResult?(FlutterError(code: "errorConnecting", message: "client id or redirectUrl is invalid", details: nil))
|
|
connectionStatusHandler?.tokenResult?(FlutterError(code: "errorConnecting", message: "client id or redirectUrl is invalid", details: nil))
|
|
connectionStatusHandler?.connectionResult = nil
|
|
connectionStatusHandler?.tokenResult = nil
|
|
return false
|
|
}
|
|
|
|
setAccessTokenFromURL(url: url)
|
|
return false
|
|
}
|
|
|
|
private func setAccessTokenFromURL(url: URL) {
|
|
guard let appRemote = appRemote else {
|
|
connectionStatusHandler?.connectionResult?(FlutterError(code: "errorConnection", message: "AppRemote is null", details: nil))
|
|
connectionStatusHandler?.tokenResult?(FlutterError(code: "errorConnection", message: "AppRemote is null", details: nil))
|
|
connectionStatusHandler?.connectionResult = nil
|
|
connectionStatusHandler?.tokenResult = nil
|
|
return
|
|
}
|
|
|
|
guard let token = appRemote.authorizationParameters(from: url)?[SPTAppRemoteAccessTokenKey] else {
|
|
connectionStatusHandler?.connectionResult?(FlutterError(code: "authenticationTokenError", message: appRemote.authorizationParameters(from: url)?[SPTAppRemoteErrorDescriptionKey], details: nil))
|
|
connectionStatusHandler?.tokenResult?(FlutterError(code: "authenticationTokenError", message: appRemote.authorizationParameters(from: url)?[SPTAppRemoteErrorDescriptionKey], details: nil))
|
|
connectionStatusHandler?.connectionResult = nil
|
|
connectionStatusHandler?.tokenResult = nil
|
|
return
|
|
}
|
|
|
|
appRemote.connectionParameters.accessToken = token
|
|
appRemote.connect()
|
|
}
|
|
}
|
|
|