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.

660 lines
23 KiB

import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:logger/logger.dart';
import 'enums/image_dimension_enum.dart';
import 'enums/podcast_playback_speed.dart';
import 'enums/repeat_mode_enum.dart';
import 'extensions/image_dimension_extension.dart';
import 'extensions/podcast_playback_speed_extension.dart';
import 'models/capabilities.dart';
import 'models/connection_status.dart';
import 'models/crossfade_state.dart';
import 'models/image_uri.dart';
import 'models/library_state.dart';
import 'models/player_context.dart';
import 'models/player_state.dart';
import 'models/user_status.dart';
import 'platform_channels.dart';
export 'package:spotify_sdk/enums/image_dimension_enum.dart';
export 'package:spotify_sdk/enums/podcast_playback_speed.dart';
export 'package:spotify_sdk/enums/repeat_mode_enum.dart';
export 'package:spotify_sdk/extensions/image_dimension_extension.dart';
export 'package:spotify_sdk/extensions/podcast_playback_speed_extension.dart';
///
/// [SpotifySdk] holds the functionality to connect via spotify remote or
/// get an authToken to control the spotify playback and use the functionality
/// described [here](https://pub.dev/packages/spotify_sdk#usage)
///
class SpotifySdk {
// method channel
static const MethodChannel _channel =
MethodChannel(MethodChannels.spotifySdk);
//player event channels
static const EventChannel _playerContextChannel =
EventChannel(EventChannels.playerContext);
static const EventChannel _playerStateChannel =
EventChannel(EventChannels.playerState);
// user event channels
static const EventChannel _userStatusChannel =
EventChannel(EventChannels.userStatus);
static const EventChannel _capabilitiesChannel =
EventChannel(EventChannels.capabilities);
// connection status channel
static const EventChannel _connectionStatusChannel =
EventChannel(EventChannels.connectionStatus);
//logging
static final Logger _logger = Logger(
//filter: CustomLogFilter(), // custom logfilter can be used to have logs in release mode
printer: PrettyPrinter(
methodCount: 2,
errorMethodCount: 8,
lineLength: 120,
colors: true,
printEmojis: true,
),
);
/// Connects to Spotify Remote, returning a [bool] for confirmation
///
/// Required parameters are the [clientId] and the [redirectUrl] to
/// authenticate with the Spotify Api
/// iOS specific: You can optionally pass an [accessToken] that you have persisted from a previous session. This will prevent redirecting to the Spotify if the token is still valid. It will be ignored on platforms other than iOS.
/// iOS specific: You can optionally pass a [spotifyUri]. A blank string will play the user's last song or pick a random one. It will be ignored on platforms other than iOS.
/// Throws a [PlatformException] if connecting to the remote api failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<bool> connectToSpotifyRemote(
{required String clientId,
required String redirectUrl,
String spotifyUri = '',
bool asRadio = false,
String? scope,
String playerName = 'Spotify SDK',
String? accessToken}) async {
try {
return await _channel.invokeMethod(MethodNames.connectToSpotify, {
ParamNames.clientId: clientId,
ParamNames.redirectUrl: redirectUrl,
ParamNames.playerName: playerName,
ParamNames.accessToken: accessToken,
ParamNames.scope: scope,
ParamNames.spotifyUri: spotifyUri,
ParamNames.asRadio: asRadio,
});
} on Exception catch (e) {
_logException(MethodNames.connectToSpotify, e);
rethrow;
}
}
/// Returns an access token as a [String]
///
/// Required parameters are the [clientId] and the [redirectUrl] to
/// authenticate with the Spotify Api.
/// Also you have to provide a [scope] like
/// "app-remote-control, user-modify-playback-state, playlist-read-private,
/// playlist-modify-public,user-read-currently-playing"
/// See https://developer.spotify.com/documentation/general/guides/scopes/
/// for more scopes and how to use them
/// The token can be used to communicate with the web api
/// iOS specific: You can optionally pass a [spotifyUri]. A blank string will play the user's last song or pick a random one. It will be ignored on platforms other than iOS.
/// Throws a [PlatformException] if retrieving the access token
/// failed.
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<String> getAccessToken(
{required String clientId,
required String redirectUrl,
String spotifyUri = '',
bool asRadio = false,
String? scope}) async {
try {
final authorization =
await _channel.invokeMethod(MethodNames.getAccessToken, {
ParamNames.clientId: clientId,
ParamNames.redirectUrl: redirectUrl,
ParamNames.scope: scope,
ParamNames.spotifyUri: spotifyUri,
ParamNames.asRadio: asRadio,
});
return authorization.toString();
} on Exception catch (e) {
_logException(MethodNames.getAccessToken, e);
rethrow;
}
}
@Deprecated('Use [getAccessToken]')
static Future<String> getAuthenticationToken(
{required String clientId,
required String redirectUrl,
String spotifyUri = '',
bool asRadio = false,
String? scope}) =>
getAccessToken(
clientId: clientId,
redirectUrl: redirectUrl,
spotifyUri: spotifyUri,
asRadio: asRadio,
scope: scope,
);
/// Logs the user out and disconnects the app from the users spotify account
///
/// Throws a [PlatformException] if disconnect failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<bool> disconnect() async {
try {
return await _channel.invokeMethod(MethodNames.disconnectFromSpotify);
} on Exception catch (e) {
_logException(MethodNames.disconnectFromSpotify, e);
rethrow;
}
}
/// Gets the current [CrossfadeState]
///
/// Throws a [PlatformException] getting the crossfadeState failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<CrossfadeState?> getCrossFadeState() async {
try {
var crossfadeStateJson =
await (_channel.invokeMethod<String>(MethodNames.getCrossfadeState));
if (crossfadeStateJson == null) {
return null;
}
var crossfadeStateMap =
jsonDecode(crossfadeStateJson) as Map<String, dynamic>;
var crossfadeState = CrossfadeState.fromJson(crossfadeStateMap);
return crossfadeState;
} on Exception catch (e) {
_logException(MethodNames.getCrossfadeState, e);
rethrow;
}
}
/// Gets the current [PlayerState]
///
/// Throws a [PlatformException] getting the playerState failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<PlayerState?> getPlayerState() async {
try {
var playerStateJson =
await (_channel.invokeMethod<String>(MethodNames.getPlayerState));
if (playerStateJson == null) {
return null;
}
var playerStateMap = jsonDecode(playerStateJson) as Map<String, dynamic>;
var playerState = PlayerState.fromJson(playerStateMap);
return playerState;
} on Exception catch (e) {
_logException(MethodNames.getPlayerState, e);
rethrow;
}
}
/// Queues the given [spotifyUri]
///
/// The [spotifyUri] can be an artist, album, playlist and track
/// Throws a [PlatformException] if queing failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future queue({required String spotifyUri}) async {
try {
await _channel.invokeMethod(
MethodNames.queueTrack, {ParamNames.spotifyUri: spotifyUri});
} on Exception catch (e) {
_logException(MethodNames.queueTrack, e);
rethrow;
}
}
/// Plays the given [spotifyUri]
///
/// The [spotifyUri] can be an artist, album, playlist and track
/// On iOS set [asRadio] to true to start radio for track URI. Default: false
/// Throws a [PlatformException] if playing failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future play({
required String spotifyUri,
bool asRadio = false,
}) async {
try {
await _channel.invokeMethod(MethodNames.play, {
ParamNames.spotifyUri: spotifyUri,
ParamNames.asRadio: asRadio,
});
} on Exception catch (e) {
_logException(MethodNames.play, e);
rethrow;
}
}
/// Pauses the current playing track
///
/// Throws a [PlatformException] if pausing failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future pause() async {
try {
await _channel.invokeMethod(MethodNames.pause);
} on Exception catch (e) {
_logException(MethodNames.pause, e);
rethrow;
}
}
/// Resumes the current paused track
///
/// Throws a [PlatformException] if resuming failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future resume() async {
try {
await _channel.invokeMethod(MethodNames.resume);
} on Exception catch (e) {
_logException(MethodNames.resume, e);
rethrow;
}
}
/// Sets the playbackSpeed of the Podcast
///
/// The podcast playback speed can be controlled via [podcastPlaybackSpeed].
/// This can only be set if the podcast is played on the local device
/// the same device the app is running on.
/// Throws a [PlatformException] if resuming failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future setPodcastPlaybackSpeed(
{required PodcastPlaybackSpeed podcastPlaybackSpeed}) async {
try {
await _channel.invokeMethod(MethodNames.setPodcastPlaybackSpeed,
{ParamNames.podcastPlaybackSpeed: podcastPlaybackSpeed.value});
} on Exception catch (e) {
_logException(MethodNames.resume, e);
rethrow;
}
}
/// Skips to the next track
///
/// Throws a [PlatformException] if skipping failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future skipNext() async {
try {
await _channel.invokeMethod(MethodNames.skipNext);
} on Exception catch (e) {
_logException(MethodNames.skipNext, e);
rethrow;
}
}
/// Skips to the previous track
///
/// Throws a [PlatformException] if skipping failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future skipPrevious() async {
try {
await _channel.invokeMethod(MethodNames.skipPrevious);
} on Exception catch (e) {
_logException(MethodNames.skipPrevious, e);
rethrow;
}
}
/// Skips to track at specified index in album or playlist
///
/// The [spotifyUri] can be an album or playlist
/// The [trackIndex] is the index of the track in the playlist to be played
/// Throws a [PlatformException] if skipping failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future skipToIndex({
required String spotifyUri,
required int trackIndex,
}) async {
try {
await _channel.invokeMethod(MethodNames.skipToIndex, {
ParamNames.spotifyUri: spotifyUri,
ParamNames.trackIndex: trackIndex,
});
} on Exception catch (e) {
_logException(MethodNames.skipToIndex, e);
rethrow;
}
}
/// Subscribes to the [PlayerContext] and returns it.
///
/// Throws a [PlatformException] if this fails
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Stream<PlayerContext> subscribePlayerContext() {
try {
var playerContextSubscription =
_playerContextChannel.receiveBroadcastStream();
return playerContextSubscription.asyncMap((playerContextJson) {
var playerContextMap =
jsonDecode(playerContextJson.toString()) as Map<String, dynamic>;
return PlayerContext.fromJson(playerContextMap);
});
} on Exception catch (e) {
_logException(MethodNames.subscribePlayerContext, e);
rethrow;
}
}
/// Subscribes to the [PlayerState] and returns it.
///
/// Throws a [PlatformException] if this fails
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Stream<PlayerState> subscribePlayerState() {
try {
var playerStateSubscription =
_playerStateChannel.receiveBroadcastStream();
return playerStateSubscription.asyncMap((playerStateJson) {
var playerStateMap =
jsonDecode(playerStateJson.toString()) as Map<String, dynamic>;
return PlayerState.fromJson(playerStateMap);
});
} on Exception catch (e) {
_logException(MethodNames.subscribePlayerState, e);
rethrow;
}
}
/// Subscribes to the [ConnectionStatus] and returns it.
///
/// Throws a [PlatformException] if this fails
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Stream<ConnectionStatus> subscribeConnectionStatus() {
try {
var connectionStatusSubscription =
_connectionStatusChannel.receiveBroadcastStream();
return connectionStatusSubscription.asyncMap((connectionStatusJson) {
var connectionStatusMap =
jsonDecode(connectionStatusJson.toString()) as Map<String, dynamic>;
return ConnectionStatus.fromJson(connectionStatusMap);
});
} on Exception catch (e) {
_logException(MethodNames.subscribeConnectionStatus, e);
rethrow;
}
}
/// Seeks the current track to the given [positionedMilliseconds]
///
///
/// Throws a [PlatformException] if seeking failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future seekTo({required int positionedMilliseconds}) async {
try {
await _channel.invokeMethod(MethodNames.seekTo,
{ParamNames.positionedMilliseconds: positionedMilliseconds});
} on Exception catch (e) {
_logException(MethodNames.seekTo, e);
rethrow;
}
}
/// Adds the given [relativeMilliseconds] to the current playback time.
///
/// This will add [relativeMilliseconds] to the current value of the playback
/// time. This can also be negative to rewind the current track.
/// Throws a [PlatformException] if seeking failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future seekToRelativePosition(
{required int relativeMilliseconds}) async {
try {
await _channel.invokeMethod(MethodNames.seekToRelativePosition,
{ParamNames.relativeMilliseconds: relativeMilliseconds});
} on Exception catch (e) {
_logException(MethodNames.seekToRelativePosition, e);
rethrow;
}
}
/// Switch to local device for playback
///
/// Throws a [PlatformException] if switching to local device failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future switchToLocalDevice() async {
try {
await _channel.invokeMethod(MethodNames.switchToLocalDevice);
} on Exception catch (e) {
_logException(MethodNames.switchToLocalDevice, e);
rethrow;
}
}
/// Toggles shuffle
///
/// Throws a [PlatformException] if toggling shuffle failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future toggleShuffle() async {
try {
await _channel.invokeMethod(MethodNames.toggleShuffle);
} on Exception catch (e) {
_logException(MethodNames.toggleShuffle, e);
rethrow;
}
}
/// Toggles repeat
///
/// Throws a [PlatformException] if toggling repeat failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future toggleRepeat() async {
try {
await _channel.invokeMethod(MethodNames.toggleRepeat);
} on Exception catch (e) {
_logException(MethodNames.toggleRepeat, e);
rethrow;
}
}
/// Adds the given [spotifyUri] to the users library
///
/// Throws a [PlatformException] if adding failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future addToLibrary({required String spotifyUri}) async {
try {
await _channel.invokeMethod(
MethodNames.addToLibrary, {ParamNames.spotifyUri: spotifyUri});
} on Exception catch (e) {
_logException(MethodNames.addToLibrary, e);
rethrow;
}
}
/// Removes the given [spotifyUri] from the users library
///
/// Throws a [PlatformException] if adding failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future removeFromLibrary({required String spotifyUri}) async {
try {
await _channel.invokeMethod(
MethodNames.removeFromLibrary, {ParamNames.spotifyUri: spotifyUri});
} on Exception catch (e) {
_logException(MethodNames.removeFromLibrary, e);
rethrow;
}
}
/// Gets the [Capabilities] of the current user
///
/// Throws a [PlatformException] getting the capability failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<Capabilities?> getCapabilities(
{required String spotifyUri}) async {
try {
var capabilitiesJson =
await _channel.invokeMethod<String>(MethodNames.getCapabilities);
if (capabilitiesJson!.isNotEmpty) {
var capabilitiesMap =
jsonDecode(capabilitiesJson) as Map<String, dynamic>;
return Capabilities.fromJson(capabilitiesMap);
}
return null;
} on Exception catch (e) {
_logException(MethodNames.getCapabilities, e);
rethrow;
}
}
/// Gets the [LibraryState] of the given [spotifyUri]
///
/// Throws a [PlatformException] when getting the library state failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<LibraryState?> getLibraryState(
{required String spotifyUri}) async {
try {
var libraryStateJson = await (_channel.invokeMethod<String>(
MethodNames.getLibraryState, {ParamNames.spotifyUri: spotifyUri}));
if (libraryStateJson == null) {
return null;
}
var libraryStateMap =
jsonDecode(libraryStateJson) as Map<String, dynamic>;
return LibraryState.fromJson(libraryStateMap);
} on Exception catch (e) {
_logException(MethodNames.getLibraryState, e);
rethrow;
}
}
/// Subscribes to the [Capabilities] of the current user
///
/// Throws a [PlatformException] getting the capability failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Stream<Capabilities> subscribeCapabilities() {
try {
var capabilitiesSubscription =
_capabilitiesChannel.receiveBroadcastStream();
return capabilitiesSubscription.asyncMap((capabilitiesJson) {
var capabilitiesMap =
jsonDecode(capabilitiesJson.toString()) as Map<String, dynamic>;
return Capabilities.fromJson(capabilitiesMap);
});
} on Exception catch (e) {
_logException(MethodNames.subscribePlayerContext, e);
rethrow;
}
}
/// Subscribes to the [UserStatus]
///
/// Throws a [PlatformException] when getting the userStatus failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Stream<UserStatus> subscribeUserStatus() {
try {
var userStatusSubscription = _userStatusChannel.receiveBroadcastStream();
return userStatusSubscription.asyncMap((userStatusJson) {
var userStatusMap =
jsonDecode(userStatusJson.toString()) as Map<String, dynamic>;
return UserStatus.fromJson(userStatusMap);
});
} on Exception catch (e) {
_logException(MethodNames.subscribePlayerContext, e);
rethrow;
}
}
/// Gets an image from a specified [imageUri]
///
/// The size of the image can be controlled via the [dimension].
/// If no [dimension] is given the default value of [ImageDimension.medium]
/// will be used
/// Throws a [PlatformException] if adding failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future<Uint8List?> getImage(
{required ImageUri imageUri,
ImageDimension dimension = ImageDimension.medium}) async {
try {
return _channel.invokeMethod(MethodNames.getImage, {
ParamNames.imageUri: imageUri.raw,
ParamNames.imageDimension: dimension.value
});
} on Exception catch (e) {
_logException(MethodNames.getImage, e);
rethrow;
}
}
/// Sets the shuffle mode
///
/// Set [shuffle] to true or false.
/// Throws a [PlatformException] if adding failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future setShuffle({required bool shuffle}) async {
try {
return _channel.invokeMethod(MethodNames.setShuffle, {
ParamNames.shuffle: shuffle,
});
} on Exception catch (e) {
_logException(MethodNames.setShuffle, e);
rethrow;
}
}
/// Sets the repeat mode
///
/// Set [repeatMode] to a value of [RepeatMode] either [off, track, context].
/// Throws a [PlatformException] if adding failed
/// Throws a [MissingPluginException] if the method is not implemented on
/// the native platforms.
static Future setRepeatMode({required RepeatMode repeatMode}) async {
try {
return _channel.invokeMethod(
MethodNames.setRepeatMode, {ParamNames.repeatMode: repeatMode.index});
} on Exception catch (e) {
_logException(MethodNames.setRepeatMode, e);
rethrow;
}
}
static void _logException(String method, Exception e) {
if (e is PlatformException) {
var message = e.message ?? '';
message += e.details != null ? '\n${e.details}' : '';
_logger.e('$method failed with: $message');
} else if (e is MissingPluginException) {
_logger.e('$method not implemented');
} else {
_logger.e('$method throws unhandled exception: ${e.toString()}');
}
}
}