303 lines
7.6 KiB
TypeScript
303 lines
7.6 KiB
TypeScript
import axios from 'axios'
|
|
|
|
const api = axios.create({
|
|
baseURL: '/api',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
|
|
api.interceptors.request.use((config) => {
|
|
const token = localStorage.getItem('vynl_token')
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
}
|
|
return config
|
|
})
|
|
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
if (error.response?.status === 401) {
|
|
localStorage.removeItem('vynl_token')
|
|
window.location.href = '/login'
|
|
}
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
// Types
|
|
export interface TokenResponse {
|
|
access_token: string
|
|
token_type: string
|
|
}
|
|
|
|
export interface UserResponse {
|
|
id: string
|
|
email: string
|
|
name: string
|
|
is_pro: boolean
|
|
daily_recommendations_remaining: number
|
|
spotify_connected: boolean
|
|
created_at: string
|
|
}
|
|
|
|
export interface PlaylistResponse {
|
|
id: string
|
|
name: string
|
|
source: string
|
|
track_count: number
|
|
image_url: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface TrackItem {
|
|
id: string
|
|
title: string
|
|
artist: string
|
|
album: string
|
|
duration_ms: number
|
|
image_url: string | null
|
|
spotify_url: string | null
|
|
}
|
|
|
|
export interface TasteProfile {
|
|
top_genres: { name: string; weight: number }[]
|
|
energy: number
|
|
mood: number
|
|
danceability: number
|
|
acousticness: number
|
|
instrumentalness: number
|
|
}
|
|
|
|
export interface PlaylistDetailResponse {
|
|
id: string
|
|
name: string
|
|
source: string
|
|
track_count: number
|
|
image_url: string | null
|
|
tracks: TrackItem[]
|
|
taste_profile: TasteProfile | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface SpotifyPlaylistItem {
|
|
id: string
|
|
name: string
|
|
track_count: number
|
|
image_url: string | null
|
|
owner: string
|
|
}
|
|
|
|
export interface RecommendationItem {
|
|
id: string
|
|
title: string
|
|
artist: string
|
|
album: string
|
|
image_url: string | null
|
|
spotify_url: string | null
|
|
bandcamp_url: string | null
|
|
youtube_url: string | null
|
|
reason: string
|
|
saved: boolean
|
|
disliked: boolean
|
|
created_at: string
|
|
}
|
|
|
|
export interface RecommendationResponse {
|
|
recommendations: RecommendationItem[]
|
|
remaining_this_week: number
|
|
}
|
|
|
|
export interface TasteProfileArtist {
|
|
name: string
|
|
track_count: number
|
|
genre: string
|
|
}
|
|
|
|
export interface TasteProfileResponse {
|
|
genre_breakdown: { name: string; percentage: number }[]
|
|
audio_features: {
|
|
energy: number
|
|
danceability: number
|
|
valence: number
|
|
acousticness: number
|
|
avg_tempo: number
|
|
}
|
|
personality: {
|
|
label: string
|
|
description: string
|
|
icon: string
|
|
}
|
|
top_artists: TasteProfileArtist[]
|
|
track_count: number
|
|
playlist_count: number
|
|
}
|
|
|
|
// Auth
|
|
export const register = (email: string, name: string, password: string) =>
|
|
api.post<TokenResponse>('/auth/register', { email, name, password }).then((r) => r.data)
|
|
|
|
export const login = (email: string, password: string) =>
|
|
api.post<TokenResponse>('/auth/login', { email, password }).then((r) => r.data)
|
|
|
|
export const getMe = () =>
|
|
api.get<UserResponse>('/auth/me').then((r) => r.data)
|
|
|
|
// Spotify OAuth
|
|
export const getSpotifyAuthUrl = () =>
|
|
api.get<{ url: string; state: string }>('/auth/spotify/authorize').then((r) => r.data)
|
|
|
|
export const spotifyCallback = (code: string) =>
|
|
api.post<TokenResponse>('/auth/spotify/callback', { code }).then((r) => r.data)
|
|
|
|
// Playlists
|
|
export const getPlaylists = () =>
|
|
api.get<PlaylistResponse[]>('/playlists').then((r) => r.data)
|
|
|
|
export const getPlaylist = (id: string) =>
|
|
api.get<PlaylistDetailResponse>(`/playlists/${id}`).then((r) => r.data)
|
|
|
|
export const deletePlaylist = (id: string) =>
|
|
api.delete(`/playlists/${id}`).then((r) => r.data)
|
|
|
|
// Spotify Import
|
|
export const getSpotifyPlaylists = () =>
|
|
api.get<SpotifyPlaylistItem[]>('/spotify/playlists').then((r) => r.data)
|
|
|
|
export const importSpotifyPlaylist = (playlistId: string) =>
|
|
api.post<PlaylistDetailResponse>('/spotify/import', { playlist_id: playlistId }).then((r) => r.data)
|
|
|
|
// Recommendations
|
|
export const generateRecommendations = (
|
|
playlistId?: string,
|
|
query?: string,
|
|
bandcampMode?: boolean,
|
|
mode?: string,
|
|
adventurousness?: number,
|
|
exclude?: string,
|
|
count?: number,
|
|
) =>
|
|
api.post<RecommendationResponse>('/recommendations/generate', {
|
|
playlist_id: playlistId,
|
|
query,
|
|
bandcamp_mode: bandcampMode || false,
|
|
mode: mode || 'discover',
|
|
adventurousness: adventurousness ?? 3,
|
|
exclude: exclude || undefined,
|
|
count: count ?? 5,
|
|
}).then((r) => r.data)
|
|
|
|
export const getRecommendationHistory = () =>
|
|
api.get<RecommendationItem[]>('/recommendations/history').then((r) => r.data)
|
|
|
|
export const getSavedRecommendations = () =>
|
|
api.get<RecommendationItem[]>('/recommendations/saved').then((r) => r.data)
|
|
|
|
export const toggleSaveRecommendation = (id: string) =>
|
|
api.post<{ saved: boolean }>(`/recommendations/${id}/save`).then((r) => r.data)
|
|
|
|
export const dislikeRecommendation = (id: string) =>
|
|
api.post<{ disliked: boolean }>(`/recommendations/${id}/dislike`).then((r) => r.data)
|
|
|
|
// YouTube Music Import
|
|
export interface YouTubeTrackResult {
|
|
title: string
|
|
artist: string
|
|
album: string | null
|
|
youtube_id: string | null
|
|
image_url: string | null
|
|
}
|
|
|
|
export const importYouTubePlaylist = (url: string) =>
|
|
api.post<PlaylistDetailResponse>('/youtube-music/import', { url }).then((r) => r.data)
|
|
|
|
export const searchYouTubeMusic = (query: string) =>
|
|
api.post<YouTubeTrackResult[]>('/youtube-music/search', { query }).then((r) => r.data)
|
|
|
|
// Last.fm Import
|
|
export interface LastfmPreviewTrack {
|
|
title: string
|
|
artist: string
|
|
playcount: number
|
|
image_url: string | null
|
|
}
|
|
|
|
export interface LastfmPreviewResponse {
|
|
display_name: string
|
|
track_count: number
|
|
sample_tracks: LastfmPreviewTrack[]
|
|
}
|
|
|
|
export const previewLastfm = (username: string) =>
|
|
api.get<LastfmPreviewResponse>('/lastfm/preview', { params: { username } }).then((r) => r.data)
|
|
|
|
export const importLastfm = (username: string, period: string) =>
|
|
api.post<PlaylistDetailResponse>('/lastfm/import', { username, period }).then((r) => r.data)
|
|
|
|
// Manual Import (paste songs)
|
|
export const importPastedSongs = (name: string, text: string) =>
|
|
api.post<PlaylistDetailResponse>('/import/paste', { name, text }).then((r) => r.data)
|
|
|
|
// Billing
|
|
export interface BillingStatusResponse {
|
|
is_pro: boolean
|
|
subscription_status: string | null
|
|
current_period_end: number | null
|
|
}
|
|
|
|
export const createCheckout = () =>
|
|
api.post<{ url: string }>('/billing/create-checkout').then((r) => r.data)
|
|
|
|
export const createBillingPortal = () =>
|
|
api.post<{ url: string }>('/billing/portal').then((r) => r.data)
|
|
|
|
export const getBillingStatus = () =>
|
|
api.get<BillingStatusResponse>('/billing/status').then((r) => r.data)
|
|
|
|
// Bandcamp
|
|
export interface BandcampRelease {
|
|
title: string
|
|
artist: string
|
|
art_url: string | null
|
|
bandcamp_url: string
|
|
genre: string
|
|
item_type: string
|
|
}
|
|
|
|
export const discoverBandcamp = (tags: string, sort: string = 'new', page: number = 1) =>
|
|
api.get<BandcampRelease[]>('/bandcamp/discover', { params: { tags, sort, page } }).then((r) => r.data)
|
|
|
|
export const getBandcampTags = () =>
|
|
api.get<string[]>('/bandcamp/tags').then((r) => r.data)
|
|
|
|
// Playlist Fix
|
|
export interface OutlierTrack {
|
|
track_number: number
|
|
artist: string
|
|
title: string
|
|
reason: string
|
|
}
|
|
|
|
export interface ReplacementTrack {
|
|
title: string
|
|
artist: string
|
|
album: string | null
|
|
reason: string
|
|
}
|
|
|
|
export interface PlaylistFixResponse {
|
|
playlist_vibe: string
|
|
outliers: OutlierTrack[]
|
|
replacements: ReplacementTrack[]
|
|
}
|
|
|
|
export const fixPlaylist = (playlistId: string) =>
|
|
api.post<PlaylistFixResponse>(`/playlists/${playlistId}/fix`).then((r) => r.data)
|
|
|
|
// Taste Profile
|
|
export const getTasteProfile = () =>
|
|
api.get<TasteProfileResponse>('/profile/taste').then((r) => r.data)
|
|
|
|
export default api
|