spotify state resume/capture

This commit is contained in:
yohlo
2025-09-12 11:34:21 -05:00
parent 0169468114
commit cf09014d50
9 changed files with 366 additions and 16 deletions

View File

@@ -2,6 +2,7 @@ import type {
SpotifyDevice,
SpotifyDevicesResponse,
SpotifyPlaybackState,
SpotifyPlaybackSnapshot,
SpotifyError,
} from './types';
@@ -125,4 +126,74 @@ export class SpotifyWebApiClient {
updateAccessToken(accessToken: string): void {
this.accessToken = accessToken;
}
async createPlaybackSnapshot(): Promise<SpotifyPlaybackSnapshot | null> {
const state = await this.getPlaybackState();
if (!state || !state.item) {
return null;
}
const snapshot: SpotifyPlaybackSnapshot = {
contextUri: state.context?.uri,
trackUri: state.item.external_urls.spotify,
trackId: state.item.id,
positionMs: state.progress_ms || 0,
shuffleState: state.shuffle_state,
repeatState: state.repeat_state,
volumePercent: state.device.volume_percent || undefined,
deviceId: state.device.id,
timestamp: Date.now(),
isPlaying: state.is_playing,
trackName: state.item.name,
artistName: state.item.artists.map(a => a.name).join(', '),
albumName: state.item.album.name,
albumImageUrl: state.item.album.images[2]?.url,
};
return snapshot;
}
async restorePlaybackSnapshot(snapshot: SpotifyPlaybackSnapshot): Promise<void> {
if (snapshot.deviceId) {
await this.transferPlayback(snapshot.deviceId, false);
await new Promise(resolve => setTimeout(resolve, 500));
}
const playbackRequest: any = {
position_ms: snapshot.positionMs,
};
if (snapshot.contextUri && snapshot.trackId) {
playbackRequest.context_uri = snapshot.contextUri;
playbackRequest.offset = { uri: `spotify:track:${snapshot.trackId}` };
} else if (snapshot.trackId) {
playbackRequest.uris = [`spotify:track:${snapshot.trackId}`];
}
const endpoint = snapshot.deviceId
? `/me/player/play?device_id=${snapshot.deviceId}`
: '/me/player/play';
await this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(playbackRequest),
});
await Promise.all([
this.request(`/me/player/shuffle?state=${snapshot.shuffleState}`, {
method: 'PUT',
}),
this.request(`/me/player/repeat?state=${snapshot.repeatState}`, {
method: 'PUT',
}),
snapshot.volumePercent !== undefined
? this.setVolume(snapshot.volumePercent)
: Promise.resolve(),
]);
if (!snapshot.isPlaying) {
await new Promise(resolve => setTimeout(resolve, 1000));
await this.pause();
}
}
}

View File

@@ -82,6 +82,10 @@ export interface SpotifyContextType extends SpotifyAuthState {
isLoading: boolean;
error: string | null;
capturedState: SpotifyPlaybackSnapshot | null;
isCaptureLoading: boolean;
isResumeLoading: boolean;
login: () => void;
logout: () => void;
@@ -95,6 +99,10 @@ export interface SpotifyContextType extends SpotifyAuthState {
setActiveDevice: (deviceId: string) => Promise<void>;
refreshPlaybackState: () => Promise<void>;
capturePlaybackState: () => Promise<void>;
resumePlaybackState: () => Promise<void>;
clearCapturedState: () => void;
}
export interface PKCEState {
@@ -105,4 +113,21 @@ export interface PKCEState {
export interface SpotifyDevicesResponse {
devices: SpotifyDevice[];
}
export interface SpotifyPlaybackSnapshot {
contextUri?: string;
trackUri?: string;
trackId?: string;
positionMs: number;
shuffleState: boolean;
repeatState: 'off' | 'context' | 'track';
volumePercent?: number;
deviceId?: string;
timestamp: number;
isPlaying: boolean;
trackName?: string;
artistName?: string;
albumName?: string;
albumImageUrl?: string;
}