recommendations.ts•7.8 kB
import type { SpotifyClient } from "../spotify-client.js";
import type { SpotifyTrack, SpotifyArtist } from "../types.js";
import { getRecentlyPlayedTracks, type RecentlyPlayedResponse } from "./library.js";
export interface RecommendationsParams {
seed_tracks?: string[];
seed_artists?: string[];
seed_genres?: string[];
limit?: number;
target_energy?: number;
target_danceability?: number;
target_valence?: number;
use_recent_listening?: boolean; // New parameter to automatically use recent listening
}
export interface RecommendationsResponse {
tracks: SpotifyTrack[];
seeds: Array<{
afterFilteringSize: number;
afterRelinkingSize: number;
href: string | null;
id: string;
initialPoolSize: number;
type: string;
}>;
}
interface SearchResponse {
tracks: {
items: SpotifyTrack[];
};
}
/**
* Get track recommendations based on seeds using Search API
* Note: Spotify deprecated the Recommendations API. This uses search with filters as an alternative.
*/
export async function getRecommendations(
client: SpotifyClient,
params: RecommendationsParams
): Promise<RecommendationsResponse> {
let {
seed_tracks,
seed_artists,
seed_genres,
limit = 10,
target_energy,
target_danceability,
target_valence,
use_recent_listening = false,
} = params;
// If use_recent_listening is true and no seeds provided, use recently played tracks
if (
use_recent_listening &&
(!seed_tracks || seed_tracks.length === 0) &&
(!seed_artists || seed_artists.length === 0)
) {
try {
const recentTracks = await getRecentlyPlayedTracks(client, 10);
if (recentTracks.items.length > 0) {
// Extract unique artist IDs from recent tracks
const artistIds = new Set<string>();
const trackIds = new Set<string>();
recentTracks.items.forEach(item => {
trackIds.add(item.track.id);
item.track.artists.forEach(artist => {
artistIds.add(artist.id);
});
});
// Use up to 2 tracks and 2 artists as seeds
seed_tracks = Array.from(trackIds).slice(0, 2);
seed_artists = Array.from(artistIds).slice(0, 2);
}
} catch (error) {
console.error("Failed to get recent tracks for recommendations:", error);
// Continue without recent listening data
}
}
// At least one seed is required
if (
(!seed_tracks || seed_tracks.length === 0) &&
(!seed_artists || seed_artists.length === 0) &&
(!seed_genres || seed_genres.length === 0)
) {
throw new Error(
"At least one seed (track, artist, or genre) is required for recommendations. " +
"You can set use_recent_listening=true to automatically use your recent listening history."
);
}
// Build search query using filters
const queryParts: string[] = [];
// If we have seed tracks, get their details to build a better query
if (seed_tracks && seed_tracks.length > 0) {
try {
// Get track details to extract artist and genre info
const trackDetails = await client.get<SpotifyTrack>(
`/tracks/${seed_tracks[0]}`
);
if (trackDetails.artists && trackDetails.artists.length > 0) {
queryParts.push(`artist:${trackDetails.artists[0].name}`);
}
} catch (error) {
// If track lookup fails, continue with other seeds
console.error("Failed to get track details:", error);
}
}
// Add artist names if provided
if (seed_artists && seed_artists.length > 0) {
try {
const artistDetails = await client.get<SpotifyArtist>(
`/artists/${seed_artists[0]}`
);
queryParts.push(`artist:${artistDetails.name}`);
} catch (error) {
console.error("Failed to get artist details:", error);
}
}
// Add genre filters
if (seed_genres && seed_genres.length > 0) {
queryParts.push(`genre:${seed_genres[0]}`);
}
// Build query based on mood parameters
let moodQuery = "";
if (target_energy !== undefined || target_danceability !== undefined || target_valence !== undefined) {
// Use mood-based keywords
if (target_energy !== undefined && target_energy > 0.7) {
moodQuery = "energetic upbeat";
} else if (target_energy !== undefined && target_energy < 0.3) {
moodQuery = "calm relaxing";
}
if (target_valence !== undefined && target_valence > 0.7) {
moodQuery += " happy";
} else if (target_valence !== undefined && target_valence < 0.3) {
moodQuery += " melancholic";
}
}
// Combine query parts
let finalQuery = moodQuery || "music";
if (queryParts.length > 0) {
finalQuery = `${moodQuery} ${queryParts.join(" ")}`.trim();
}
// Use tag:new for recent music to get fresh recommendations
if (!seed_tracks && !seed_artists) {
finalQuery += " tag:new";
}
// Search for tracks
const searchParams = {
q: finalQuery,
type: "track",
limit: Math.min(limit * 2, 50), // Get more results to filter
};
const response = await client.get<SearchResponse>("/search", searchParams);
// Filter out seed tracks if provided
let tracks = response.tracks.items;
if (seed_tracks && seed_tracks.length > 0) {
tracks = tracks.filter((track) => !seed_tracks.includes(track.id));
}
// Limit results
tracks = tracks.slice(0, limit);
// Format response to match old recommendations API structure
return {
tracks,
seeds: [
{
afterFilteringSize: tracks.length,
afterRelinkingSize: tracks.length,
href: null,
id: seed_genres?.[0] || seed_artists?.[0] || seed_tracks?.[0] || "unknown",
initialPoolSize: response.tracks.items.length,
type: seed_genres?.[0] ? "genre" : seed_artists?.[0] ? "artist" : "track",
},
],
};
}
/**
* Get available genre seeds
* Note: Since the recommendations API is deprecated, we return a curated list of common genres
*/
export async function getAvailableGenreSeeds(
client: SpotifyClient
): Promise<string[]> {
// Return common Spotify genres that work well with search
return [
"acoustic",
"afrobeat",
"alt-rock",
"alternative",
"ambient",
"blues",
"chill",
"classical",
"country",
"dance",
"dancehall",
"deep-house",
"disco",
"drum-and-bass",
"dub",
"dubstep",
"edm",
"electro",
"electronic",
"emo",
"folk",
"funk",
"gospel",
"goth",
"grunge",
"guitar",
"hard-rock",
"hardcore",
"hip-hop",
"house",
"indie",
"indie-pop",
"industrial",
"jazz",
"k-pop",
"latin",
"latino",
"metal",
"metalcore",
"minimal-techno",
"party",
"piano",
"pop",
"pop-film",
"punk",
"punk-rock",
"r-n-b",
"rainy-day",
"reggae",
"reggaeton",
"rock",
"rock-n-roll",
"rockabilly",
"romance",
"sad",
"salsa",
"samba",
"singer-songwriter",
"ska",
"sleep",
"soul",
"study",
"summer",
"synth-pop",
"tango",
"techno",
"trance",
"trip-hop",
"world-music",
];
}
/**
* Format recommendations for display
*/
export function formatRecommendations(
response: RecommendationsResponse
): string {
if (!response.tracks || response.tracks.length === 0) {
return "No recommendations found.";
}
const lines = ["**Recommended Tracks:**", ""];
response.tracks.forEach((track, i) => {
const artists = track.artists.map((a) => a.name).join(", ");
lines.push(`${i + 1}. **${track.name}** by ${artists}`);
lines.push(` Album: ${track.album.name}`);
lines.push(` URI: ${track.uri}`);
lines.push(` Link: ${track.external_urls.spotify}`);
lines.push("");
});
return lines.join("\n");
}