Add discovery modes, personalization controls, taste profile page, updated pricing
- Discovery modes: Sonic Twin, Era Bridge, Deep Cuts, Rising Artists - Discovery dial (Safe to Adventurous slider) - Block genres/moods exclusion - Thumbs down/dislike on recommendations - My Taste page with Genre DNA breakdown, audio feature meters, listening personality - Updated pricing: Free (5/week), Premium ($6.99/mo), Family ($12.99/mo coming soon) - Weekly rate limiting instead of daily - Alembic migration for new fields
This commit is contained in:
@@ -99,12 +99,38 @@ export interface RecommendationItem {
|
||||
bandcamp_url: string | null
|
||||
reason: string
|
||||
saved: boolean
|
||||
disliked: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface RecommendationResponse {
|
||||
recommendations: RecommendationItem[]
|
||||
remaining_today: number
|
||||
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
|
||||
@@ -142,11 +168,21 @@ 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) =>
|
||||
export const generateRecommendations = (
|
||||
playlistId?: string,
|
||||
query?: string,
|
||||
bandcampMode?: boolean,
|
||||
mode?: string,
|
||||
adventurousness?: number,
|
||||
exclude?: string,
|
||||
) =>
|
||||
api.post<RecommendationResponse>('/recommendations/generate', {
|
||||
playlist_id: playlistId,
|
||||
query,
|
||||
bandcamp_mode: bandcampMode || false,
|
||||
mode: mode || 'discover',
|
||||
adventurousness: adventurousness ?? 3,
|
||||
exclude: exclude || undefined,
|
||||
}).then((r) => r.data)
|
||||
|
||||
export const getRecommendationHistory = () =>
|
||||
@@ -158,6 +194,9 @@ export const getSavedRecommendations = () =>
|
||||
export const toggleSaveRecommendation = (id: string) =>
|
||||
api.post<{ saved: boolean }>(`/recommendations/${id}/toggle-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
|
||||
@@ -239,4 +278,8 @@ export async function getBandcampEmbed(url: string): Promise<BandcampEmbed> {
|
||||
return data
|
||||
}
|
||||
|
||||
// Taste Profile
|
||||
export const getTasteProfile = () =>
|
||||
api.get<TasteProfileResponse>('/profile/taste').then((r) => r.data)
|
||||
|
||||
export default api
|
||||
|
||||
Reference in New Issue
Block a user