Skip to content

Instantly share code, notes, and snippets.

@Platekun
Created January 19, 2020 16:45
Show Gist options
  • Save Platekun/5c37a9796547862c7299ad6d4bbff5a9 to your computer and use it in GitHub Desktop.
Save Platekun/5c37a9796547862c7299ad6d4bbff5a9 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
const WEB_PLAYBACK_API = 'https://api.spotify.com/v1/me/player';
const InitialState = '@Initial';
const ReadyState = '@Ready';
const NotReadyState = '@NotReadyState';
const IdleState = '@Idle';
const PlayState = '@Play';
const RequestingToPlayTrackState = '@Play/Track/Request';
const RequestingToPlayAlbumState = '@Play/Album/Request';
const RequestingToResumeTrackState = '@Resume/Request';
const PlayingState = '@Play/Success';
const FailedToPlayState = '@Play/Error';
const PauseState = '@Pause';
const RequestingToPauseTrackState = '@Pause/Request';
const PausedState = '@Pause/Success';
const ResettingCurrentTrackProgressState = '@Pause/ResettingCurrentTrackProgressState';
const StoppingState = '@Stop';
const RequestingToSkipPlaybackToPreviousTrackState = '@RequestingToSkipPlaybackToPreviousTrack';
const RequestingToSkipPlaybackToNextTrackState = '@RequestingToSkipPlaybackToNextTrack';
const InitializationErrorState = '@InitializationError';
const AccountErrorState = '@AccountError';
const AuthenticationErrorState = '@AuthenticationError';
const PlaybackErrorState = '@PlaybackError';
const TrackSelectedEvent = 'TRACK_SELECTED';
const AlbumSelectedEvent = 'ALBUM_SELECTED';
const TrackPausedEvent = 'TRACK_PAUSED';
const FailedToPauseEvent = 'FAILED_TO_PAUSE_TRACK';
const TrackResumedEvent = 'TRACK_RESUMED';
const TrackStoppedEvent = 'TRACK_STOPPED';
const FailedToStopEvent = 'FAILED_TO_STOP_TRACK';
const NextTrackRequestedEvent = 'NEXT_TRACK_REQUESTED';
const PreviousTrackRequestedEvent = 'PREVIOUS_TRACK_REQUESTED';
const SkippedPlaybackToPreviousTrackEvent = 'SKIPPED_PLAYBACK_TO_PREVIOUS_TRACK_EVENT';
const InitializationErrorOcurredEvent = 'INITIALIZATION_ERROR_OCURRED';
const PlayerReadyEvent = 'PLAYER_READY';
const PlayerNotReadyEvent = 'PLAYER_NOT_READY';
const AuthenticationErrorOcurredEvent = 'AUTHENTICATION_ERROR_OCURRED';
const AccountErrorOcurredEvent = 'ACCOUNT_ERROR_OCURRED';
const PlaybackErrorOcurredEvent = 'PLAYBACK_ERROR_OCURRED';
const PlayerStateChangedEvent = 'PLAYER_STATE_CHANGED';
const setTrackSelectionAction = 'setTrackSelection';
const setAlbumSelectionAction = 'setAlbumSelection';
const updateRetryCountAction = 'updateRetryCount';
const resetRetryCountAction = 'resetRetryCount';
const logPlayerReadyAction = 'logPlayerReady';
const saveDeviceIdAction = 'saveDeviceId';
const savePlayerInstanceAction = 'savePlayerInstance';
const logPlaybackErrorAction = 'logPlaybackError';
const logAccountErrorAction = 'logAccountError';
const logAuthenticationErrorAction = 'logAuthenticationError';
const logInitializationErrorAction = 'logInitializationError';
const logPlayerStateChangeAction = 'logPlayerStateChange';
const setPlaybackStateAction = 'setPlaybackState';
const logPlayerNotReadyAction = 'logPlayerNotReady';
const setPlayerWasBeingPlayingBeforeStoppingFlagAction = 'setPlayerWasBeingPlayingBeforeStoppingFlag';
const deletePlayerWasBeingPlayingBeforeStoppingFlagAction = 'deletePlayerWasBeingPlayingBeforeStoppingFlag';
const setPlayerWasBeingPausedBeforeStoppingFlagAction = 'setPlayerWasBeingPausedBeforeStoppingFlag';
const deletePlayerWasBeingPausedBeforeStoppingFlagAction = 'deletePlayerWasBeingPausedBeforeStoppingFlag';
const playTrackService = 'playTrack';
const playAlbumService = 'playAlbumService';
const resumeTrackService = 'resumeTrack';
const pauseTrackService = 'pauseTrack';
const skipPlaybackToPreviousTrackService = 'skipPlaybackToPreviousTrack';
const skipPlaybackToNextTrackService = 'skipPlaybackToNextTrack';
const resetCurrentTrackProgressService = 'resetCurrentTrackPRogress';
const stopTrackService = 'stopTrack';
const keepRetryingGuard = 'keepRetrying';
const isNotTheSameTrackThatIsPlaying = 'isAValidTrackToPlay';
const wasPlayingBeforeStoppingGuard = 'wasPlayingBeforeStopping';
const wasPausedBeforeStoppingGuard = 'wasPausedBeforeStopping';
const lessThanTwoSecondsHasPassedGuard = 'lessThanTwoSecondsHasPassed';
const moreThanTwoSecondsHasPassedGuard = 'moreThanTwoSecondsHasPassed';
const playbackIsPausedGuard = 'playbackIsPaused';
const playbackIsNotPausedGuard = 'playbackIsNotPaused';
const getPlaybackStateCallback = 'getPlaybackState';
async function prettifyError(res) {
let parsedResponse = await res.json();
return JSON.stringify(parsedResponse, null, 2);
}
function checkOperationParam(opts) {
let { param, message, loggerService, boom = true } = opts;
if (param === null || typeof param === 'undefined' || param === '' || param.trim() === '') {
loggerService.log(message, 'error');
if (boom) {
throw new Error(message);
}
}
}
function canSkipToPreviousTrack(ctx) {
let { playbackState } = ctx;
if (playbackState === null) {
return false;
}
let { disallows: { skipping_prev: skippingPrev }, track_window: { previous_tracks: previousTracks } } = playbackState;
return !skippingPrev || previousTracks.length !== 0;
}
async function pauseTrackServiceFn(ctx) {
let { deviceId, trackSelection, authService, loggerService } = ctx;
let token = authService.token;
let { spotifyUri } = trackSelection;
checkOperationParam({
param: deviceId,
message: 'You need a device id in order to pause a track',
loggerService
});
checkOperationParam({
param: spotifyUri,
message: 'You need a spotify URI in order to pause a track',
loggerService
});
checkOperationParam({ param: token, message: 'You need a JWT in order to pause a track', loggerService });
let response = await fetch(`${WEB_PLAYBACK_API}/pause?device_id=${deviceId}`, {
method: 'PUT',
body: JSON.stringify({ uris: [spotifyUri] }),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
let reason = await prettifyError(response);
let errorMessage = `Current track could not be paused: ${reason}`;
loggerService.log(errorMessage, 'error');
throw new Error(errorMessage);
}
loggerService.log(`Current track was paused`, 'success');
}
async function resetCurrentTrackProgressServiceFn(ctx) {
let { deviceId, authService, loggerService } = ctx;
let token = authService.token;
checkOperationParam({
param: deviceId,
message: 'You need a device id in order to reset the current track progress',
loggerService
});
checkOperationParam({
param: token,
message: 'You need a JWT in order to reset the current track track progress',
loggerService
});
let response = await fetch(`${WEB_PLAYBACK_API}/seek?device_id=${deviceId}&position_ms=${0}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authService.token}`
}
});
if (!response.ok) {
let reason = await prettifyError(response);
let errorMessage = `Failed to reset the current track progress: ${reason}`;
loggerService.log(errorMessage, 'error');
throw new Error(errorMessage);
}
loggerService.log(`Current track progress was reset`, 'success');
}
const playbackMachine = Machine({
id: 'Playback Machine',
initial: InitialState,
states: {
[InitialState]: {
on: {
[PlayerReadyEvent]: {
target: ReadyState,
actions: [logPlayerReadyAction, saveDeviceIdAction, savePlayerInstanceAction]
},
[PlayerNotReadyEvent]: {
target: NotReadyState,
actions: [logPlayerNotReadyAction]
}
}
},
[ReadyState]: {
initial: IdleState,
states: {
[IdleState]: {
on: {
[TrackSelectedEvent]: {
target: PlayState,
actions: [setTrackSelectionAction],
// ? TODO: Do I need this?
cond: isNotTheSameTrackThatIsPlaying
},
[AlbumSelectedEvent]: {
target: `${PlayState}.${RequestingToPlayAlbumState}`,
actions: [setAlbumSelectionAction]
}
}
},
[PlayState]: {
initial: RequestingToPlayTrackState,
states: {
[RequestingToPlayTrackState]: {
invoke: {
id: 'invokePlayTrack',
src: playTrackService,
onDone: {
target: PlayingState,
actions: [resetRetryCountAction]
},
onError: [
{
target: RequestingToPlayTrackState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
target: FailedToPlayState,
actions: [resetRetryCountAction]
}
]
}
},
[RequestingToPlayAlbumState]: {
invoke: {
id: 'invokePlayAlbum',
src: playAlbumService,
onDone: {
target: PlayingState,
actions: [resetRetryCountAction]
},
onError: [
{
target: RequestingToPlayAlbumState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
target: FailedToPlayState,
actions: [resetRetryCountAction]
}
]
}
},
[RequestingToResumeTrackState]: {
invoke: {
id: 'invokePauseTrack',
src: resumeTrackService,
onDone: {
target: PlayingState,
actions: [resetRetryCountAction]
},
onError: [
{
target: RequestingToResumeTrackState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
target: FailedToPlayState,
actions: [resetRetryCountAction]
}
]
}
},
[PlayingState]: {
invoke: {
id: getPlaybackStateCallback,
src: (ctx) => (callback) => {
async function getInformationAboutTheUsersCurrentPlayback() {
let { player, loggerService } = ctx;
let state = await player.getCurrentState();
if (state === null) {
return loggerService.log('No state obtained from the player. User is not playing music through the Web Playback SDK', 'error');
}
callback({ type: PlayerStateChangedEvent, state });
}
const id = setInterval(getInformationAboutTheUsersCurrentPlayback, 1000);
return () => clearInterval(id);
}
}
},
[FailedToPlayState]: {},
[RequestingToSkipPlaybackToPreviousTrackState]: {
invoke: {
id: 'skipPlaybackToPrevious',
src: skipPlaybackToPreviousTrackService,
onDone: {
target: PlayingState,
actions: [resetRetryCountAction]
},
onError: [
{
target: PlayingState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
target: PlayingState,
actions: [resetRetryCountAction]
}
]
}
},
[RequestingToSkipPlaybackToNextTrackState]: {
invoke: {
id: 'skipPlaybackToNext',
src: skipPlaybackToNextTrackService,
onDone: {
target: PlayingState,
actions: [resetRetryCountAction]
},
onError: [
{
target: PlayingState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
target: PlayingState,
actions: [resetRetryCountAction]
}
]
}
}
},
on: {
[TrackSelectedEvent]: {
target: PlayState,
actions: [setTrackSelectionAction, resetRetryCountAction],
cond: isNotTheSameTrackThatIsPlaying
},
[AlbumSelectedEvent]: {
target: `${PlayState}.${RequestingToPlayAlbumState}`,
actions: [setAlbumSelectionAction, resetRetryCountAction]
},
[TrackPausedEvent]: {
target: PauseState
},
[TrackStoppedEvent]: {
target: StoppingState,
actions: [setPlayerWasBeingPlayingBeforeStoppingFlagAction]
},
[PreviousTrackRequestedEvent]: [
{
target: `${PlayState}.${RequestingToSkipPlaybackToPreviousTrackState}`,
actions: [resetRetryCountAction],
cond: lessThanTwoSecondsHasPassedGuard
},
{
target: PlayState,
actions: [resetRetryCountAction]
}
],
[NextTrackRequestedEvent]: {
target: `${PlayState}.${RequestingToSkipPlaybackToNextTrackState}`,
actions: [resetRetryCountAction]
},
[PlayerStateChangedEvent]: [
{
target: `${PauseState}.${PausedState}`,
actions: [logPlayerStateChangeAction, setPlaybackStateAction],
cond: playbackIsPausedGuard
},
{
actions: [logPlayerStateChangeAction, setPlaybackStateAction]
}
]
}
},
[PauseState]: {
initial: RequestingToPauseTrackState,
states: {
[RequestingToPauseTrackState]: {
invoke: {
id: 'pauseTrack',
src: pauseTrackService,
onDone: {
target: PausedState,
actions: [resetRetryCountAction]
},
onError: [
{
target: RequestingToPauseTrackState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
actions: [send(FailedToPauseEvent), resetRetryCountAction]
}
]
}
},
[PausedState]: {},
[ResettingCurrentTrackProgressState]: {
invoke: {
id: 'resetCurrentTrackProgress',
src: resetCurrentTrackProgressService,
onDone: {
actions: [resetRetryCountAction]
},
onError: [
{
target: ResettingCurrentTrackProgressState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
target: PausedState,
actions: [resetRetryCountAction]
}
]
}
}
},
on: {
[FailedToPauseEvent]: `${PlayState}.${PlayingState}`,
[TrackSelectedEvent]: {
target: PlayState,
actions: [setTrackSelectionAction, resetRetryCountAction],
cond: isNotTheSameTrackThatIsPlaying
},
[AlbumSelectedEvent]: {
target: `${PlayState}.${RequestingToPlayAlbumState}`,
actions: [setAlbumSelectionAction, resetRetryCountAction]
},
[TrackResumedEvent]: {
target: `${PlayState}.${RequestingToResumeTrackState}`,
actions: [resetRetryCountAction]
},
[TrackStoppedEvent]: {
target: StoppingState,
actions: [setPlayerWasBeingPausedBeforeStoppingFlagAction]
},
[PreviousTrackRequestedEvent]: [
{
target: `${PlayState}.${RequestingToSkipPlaybackToPreviousTrackState}`,
actions: [resetRetryCountAction],
cond: lessThanTwoSecondsHasPassedGuard
},
{
target: `${PauseState}.${ResettingCurrentTrackProgressState}`,
actions: [resetRetryCountAction]
}
],
[NextTrackRequestedEvent]: {
target: `${PlayState}.${RequestingToSkipPlaybackToNextTrackState}`,
actions: [resetRetryCountAction]
},
[PlayerStateChangedEvent]: [
{
target: `${PlayState}.${PlayingState}`,
actions: [logPlayerStateChangeAction, setPlaybackStateAction],
cond: playbackIsNotPausedGuard
},
{
actions: [logPlayerStateChangeAction, setPlaybackStateAction]
}
]
}
},
[StoppingState]: {
exit: [
deletePlayerWasBeingPlayingBeforeStoppingFlagAction,
deletePlayerWasBeingPausedBeforeStoppingFlagAction
],
invoke: {
id: 'stopTrack',
src: stopTrackService,
onDone: {
target: `${PauseState}.${PausedState}`,
actions: [resetRetryCountAction]
},
onError: [
{
target: StoppingState,
actions: [updateRetryCountAction],
cond: keepRetryingGuard
},
{
target: `${PlayState}.${PlayingState}`,
actions: [send(FailedToStopEvent), resetRetryCountAction],
cond: wasPlayingBeforeStoppingGuard
},
{
target: `${PauseState}.${PausedState}`,
actions: [send(FailedToStopEvent), resetRetryCountAction],
cond: wasPausedBeforeStoppingGuard
}
]
},
on: {
[TrackSelectedEvent]: {
target: PlayState,
actions: [setTrackSelectionAction],
cond: isNotTheSameTrackThatIsPlaying
},
[AlbumSelectedEvent]: {
target: `${PlayState}.${RequestingToPlayAlbumState}`,
actions: [setAlbumSelectionAction, resetRetryCountAction]
},
[FailedToStopEvent]: [
{
target: `${PlayState}.${PlayingState}`,
cond: wasPlayingBeforeStoppingGuard
},
{
target: `${PauseState}.${PausedState}`,
cond: wasPausedBeforeStoppingGuard
}
],
[PreviousTrackRequestedEvent]: {},
[NextTrackRequestedEvent]: {}
}
}
},
on: {
[PlaybackErrorOcurredEvent]: PlaybackErrorState,
[PlayerNotReadyEvent]: {
target: NotReadyState,
actions: [logPlayerNotReadyAction]
}
}
},
[NotReadyState]: {
entry: [logPlayerNotReadyAction],
on: {
[PlayerReadyEvent]: {
target: ReadyState,
actions: [logPlayerReadyAction, saveDeviceIdAction]
}
}
},
[InitializationErrorState]: {
entry: [logInitializationErrorAction]
},
[AuthenticationErrorState]: {
entry: [logAuthenticationErrorAction]
},
[AccountErrorState]: {
entry: [logAccountErrorAction]
},
[PlaybackErrorState]: {
entry: [logPlaybackErrorAction]
}
},
on: {
[InitializationErrorOcurredEvent]: InitializationErrorState,
[AuthenticationErrorOcurredEvent]: AuthenticationErrorState,
[AccountErrorOcurredEvent]: AccountErrorState
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment