228 lines
7.1 KiB
TypeScript
228 lines
7.1 KiB
TypeScript
import SpotifyWebApi from 'spotify-web-api-js';
|
|
import { SpotifyTrack, SpotifyUser, RecentlyPlayedItem, SpotifyPlaylist } from '../types';
|
|
|
|
const SPOTIFY_CLIENT_ID = (import.meta as any).env.VITE_SPOTIFY_CLIENT_ID;
|
|
// Use same-origin callback for prod proxy server; env can override
|
|
const REDIRECT_URI = (import.meta as any).env.VITE_REDIRECT_URI || (typeof window !== 'undefined' ? `${window.location.origin}/callback.html` : 'http://localhost:3000/callback.html');
|
|
|
|
// Debug: Check what redirect URI is being used
|
|
console.log('🔍 Debug - Current redirect URI:', REDIRECT_URI);
|
|
console.log('🔍 Debug - Environment VITE_REDIRECT_URI:', (import.meta as any).env.VITE_REDIRECT_URI);
|
|
|
|
|
|
export const spotifyApi = new SpotifyWebApi();
|
|
|
|
export const getSpotifyAuthUrl = (): string => {
|
|
const scopes = [
|
|
'user-read-private',
|
|
'user-read-email',
|
|
'user-read-recently-played',
|
|
'user-top-read',
|
|
'playlist-read-private',
|
|
'playlist-read-collaborative',
|
|
'playlist-modify-public',
|
|
'playlist-modify-private',
|
|
'user-read-playback-state',
|
|
'user-modify-playback-state',
|
|
'user-read-currently-playing',
|
|
'ugc-image-upload', // Required for uploading playlist cover images
|
|
].join(' ');
|
|
|
|
const params = new URLSearchParams({
|
|
client_id: SPOTIFY_CLIENT_ID,
|
|
response_type: 'code',
|
|
redirect_uri: REDIRECT_URI,
|
|
scope: scopes,
|
|
show_dialog: 'true',
|
|
});
|
|
|
|
const authUrl = `https://accounts.spotify.com/authorize?${params.toString()}`;
|
|
console.log('🔍 Debug - Generated Spotify Auth URL:', authUrl);
|
|
return authUrl;
|
|
};
|
|
|
|
export const exchangeCodeForToken = async (code: string): Promise<{
|
|
access_token: string;
|
|
refresh_token: string;
|
|
expires_in: number;
|
|
}> => {
|
|
const body = new URLSearchParams({
|
|
grant_type: 'authorization_code',
|
|
code,
|
|
redirect_uri: REDIRECT_URI,
|
|
});
|
|
|
|
console.log('🔍 Spotify - Token exchange request:', {
|
|
code: code.substring(0, 20) + '...',
|
|
redirect_uri: REDIRECT_URI,
|
|
client_id: SPOTIFY_CLIENT_ID
|
|
});
|
|
|
|
const response = await fetch('https://accounts.spotify.com/api/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Basic ${btoa(`${SPOTIFY_CLIENT_ID}:${(import.meta as any).env.VITE_SPOTIFY_CLIENT_SECRET}`)}`,
|
|
},
|
|
body: body,
|
|
});
|
|
|
|
console.log('🔍 Spotify - Token exchange response:', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
ok: response.ok
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('🔍 Spotify - Token exchange error:', errorText);
|
|
throw new Error(`Failed to exchange code for token: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const tokenData = await response.json();
|
|
console.log('🔍 Spotify - Token exchange success:', !!tokenData.access_token);
|
|
return tokenData;
|
|
};
|
|
|
|
export const refreshAccessToken = async (refreshToken: string): Promise<{
|
|
access_token: string;
|
|
expires_in: number;
|
|
}> => {
|
|
const response = await fetch('https://accounts.spotify.com/api/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Authorization': `Basic ${btoa(`${SPOTIFY_CLIENT_ID}:${(import.meta as any).env.VITE_SPOTIFY_CLIENT_SECRET}`)}`,
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: 'refresh_token',
|
|
refresh_token: refreshToken,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to refresh access token');
|
|
}
|
|
|
|
return response.json();
|
|
};
|
|
|
|
export const initializeSpotifyApi = (accessToken: string): void => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
};
|
|
|
|
export const fetchUserProfile = async (accessToken: string): Promise<SpotifyUser> => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
return spotifyApi.getMe();
|
|
};
|
|
|
|
export const fetchRecentlyPlayed = async (accessToken: string, limit = 20): Promise<RecentlyPlayedItem[]> => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
const response = await spotifyApi.getMyRecentlyPlayedTracks({ limit });
|
|
return response.items;
|
|
};
|
|
|
|
export const fetchTopTracks = async (accessToken: string, timeRange = 'short_term', limit = 20): Promise<SpotifyTrack[]> => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
const response = await spotifyApi.getMyTopTracks({ time_range: timeRange, limit });
|
|
return response.items;
|
|
};
|
|
|
|
export const fetchTopArtists = async (accessToken: string, timeRange = 'short_term', limit = 20) => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
return spotifyApi.getMyTopArtists({ time_range: timeRange, limit });
|
|
};
|
|
|
|
export const createPlaylist = async (
|
|
accessToken: string,
|
|
userId: string,
|
|
name: string,
|
|
description: string
|
|
): Promise<SpotifyPlaylist> => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
const playlist = await spotifyApi.createPlaylist(userId, {
|
|
name,
|
|
description,
|
|
public: false,
|
|
});
|
|
return playlist;
|
|
};
|
|
|
|
export const addTracksToPlaylist = async (
|
|
accessToken: string,
|
|
playlistId: string,
|
|
trackUris: string[]
|
|
): Promise<void> => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
await spotifyApi.addTracksToPlaylist(playlistId, trackUris);
|
|
};
|
|
|
|
export const playTrack = async (accessToken: string, trackUri: string): Promise<void> => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
await spotifyApi.play({
|
|
uris: [trackUri],
|
|
});
|
|
};
|
|
|
|
export const pausePlayback = async (accessToken: string): Promise<void> => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
await spotifyApi.pause();
|
|
};
|
|
|
|
export const getCurrentPlayback = async (accessToken: string) => {
|
|
spotifyApi.setAccessToken(accessToken);
|
|
return spotifyApi.getMyCurrentPlaybackState();
|
|
};
|
|
|
|
// Utility function to extract track features for playlist generation
|
|
export const analyzeTrackFeatures = (tracks: SpotifyTrack[]) => {
|
|
const features = {
|
|
genres: new Set<string>(),
|
|
artists: new Set<string>(),
|
|
avgPopularity: 0,
|
|
totalDuration: 0,
|
|
decades: new Set<string>(),
|
|
};
|
|
|
|
tracks.forEach(track => {
|
|
// Extract genres from artists (simplified)
|
|
track.artists.forEach(artist => features.artists.add(artist.name));
|
|
|
|
// Calculate average popularity
|
|
features.avgPopularity += track.popularity;
|
|
|
|
// Add duration
|
|
features.totalDuration += track.duration_ms;
|
|
|
|
// Estimate decade from album name or other clues (simplified)
|
|
// This would typically use more sophisticated analysis
|
|
});
|
|
|
|
features.avgPopularity /= tracks.length;
|
|
|
|
return features;
|
|
};
|
|
|
|
// Generate mixed playlist based on both users' music
|
|
export const generateMixedPlaylist = (
|
|
user1Tracks: SpotifyTrack[],
|
|
user2Tracks: SpotifyTrack[],
|
|
playlistName: string = "Our Mixed Vibes"
|
|
): SpotifyTrack[] => {
|
|
const allTracks = [...user1Tracks, ...user2Tracks];
|
|
|
|
// Remove duplicates based on track ID
|
|
const uniqueTracks = allTracks.filter((track, index, self) =>
|
|
index === self.findIndex(t => t.id === track.id)
|
|
);
|
|
|
|
// Sort by popularity and mix them
|
|
const sortedByPopularity = uniqueTracks.sort((a, b) => b.popularity - a.popularity);
|
|
|
|
// Take top tracks and shuffle them for variety
|
|
const topTracks = sortedByPopularity.slice(0, 30);
|
|
const shuffled = topTracks.sort(() => Math.random() - 0.5);
|
|
|
|
return shuffled.slice(0, 25); // Return 25 tracks for the playlist
|
|
};
|