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:
root
2026-03-31 00:21:58 -05:00
parent 789de25c1a
commit 1eea237c08
17 changed files with 898 additions and 113 deletions

View File

@@ -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