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

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()
}
}