Files
vynl/frontend/src/lib/api.ts

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