spotify.ts•5.48 kB
import { BaseApiClient } from "../../core/base-api.js";
import fetch from "node-fetch";
import { loadConfig } from "../../core/config.js";
import { ToolRegistration } from "../../core/types.js";
class SpotifyClient extends BaseApiClient {
private readonly clientId: string;
private readonly clientSecret: string;
private accessToken: string | null = null;
private tokenExpiresAt = 0;
constructor(clientId: string, clientSecret: string) {
super("https://api.spotify.com/v1");
this.clientId = clientId;
this.clientSecret = clientSecret;
}
private async ensureToken(): Promise<string> {
const now = Date.now();
if (this.accessToken && now < this.tokenExpiresAt - 30000) {
return this.accessToken;
}
const auth = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
const tokenResp = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
Authorization: `Basic ${auth}`,
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ grant_type: "client_credentials" }) as any,
} as any);
if (!tokenResp.ok) {
throw new Error(`Spotify token error: ${tokenResp.status}`);
}
const data: any = await tokenResp.json();
this.accessToken = data.access_token as string;
this.tokenExpiresAt = Date.now() + ((data.expires_in as number) || 3600) * 1000;
return this.accessToken!;
}
private async authHeaders() {
const token = await this.ensureToken();
return { Authorization: `Bearer ${token}` };
}
async searchTracks(query: string, limit?: number) {
return this.request("/search", {
headers: await this.authHeaders(),
query: { q: query, type: "track", limit: limit ?? 10 },
});
}
async getTrack(trackId: string) {
return this.request(`/tracks/${trackId}`, { headers: await this.authHeaders() });
}
async createPlaylist(userId: string, name: string) {
return this.request(`/users/${userId}/playlists`, {
method: "POST",
headers: await this.authHeaders(),
body: { name, public: false },
});
}
async addTracksToPlaylist(playlistId: string, tracks: string[]) {
return this.request(`/playlists/${playlistId}/tracks`, {
method: "POST",
headers: await this.authHeaders(),
body: { uris: tracks },
});
}
}
export function registerSpotify(): ToolRegistration {
const cfg = loadConfig();
const client = new SpotifyClient(cfg.spotifyClientId || "", cfg.spotifyClientSecret || "");
return {
tools: [
{
name: "search_tracks",
description: "Search tracks on Spotify",
inputSchema: {
type: "object",
properties: { query: { type: "string" }, limit: { type: "number" } },
required: ["query"],
},
},
{
name: "get_track_info",
description: "Get Spotify track details",
inputSchema: {
type: "object",
properties: { track_id: { type: "string" } },
required: ["track_id"],
},
},
{
name: "create_playlist",
description: "Create a new playlist for a user",
inputSchema: {
type: "object",
properties: { user_id: { type: "string" }, name: { type: "string" } },
required: ["user_id", "name"],
},
},
{
name: "add_tracks_to_playlist",
description: "Add tracks to a playlist by URIs",
inputSchema: {
type: "object",
properties: { playlist_id: { type: "string" }, tracks: { type: "array", items: { type: "string" } } },
required: ["playlist_id", "tracks"],
},
},
],
handlers: {
async search_tracks(args: Record<string, unknown>) {
if (!cfg.spotifyClientId || !cfg.spotifyClientSecret) throw new Error("SPOTIFY_CLIENT_ID/SECRET are not configured");
const query = String(args.query || "");
if (!query) throw new Error("query is required");
const limit = args.limit ? Number(args.limit) : undefined;
return client.searchTracks(query, limit);
},
async get_track_info(args: Record<string, unknown>) {
if (!cfg.spotifyClientId || !cfg.spotifyClientSecret) throw new Error("SPOTIFY_CLIENT_ID/SECRET are not configured");
const trackId = String(args.track_id || "");
if (!trackId) throw new Error("track_id is required");
return client.getTrack(trackId);
},
async create_playlist(args: Record<string, unknown>) {
if (!cfg.spotifyClientId || !cfg.spotifyClientSecret) throw new Error("SPOTIFY_CLIENT_ID/SECRET are not configured");
const userId = String(args.user_id || "");
const name = String(args.name || "");
if (!userId || !name) throw new Error("user_id and name are required");
return client.createPlaylist(userId, name);
},
async add_tracks_to_playlist(args: Record<string, unknown>) {
if (!cfg.spotifyClientId || !cfg.spotifyClientSecret) throw new Error("SPOTIFY_CLIENT_ID/SECRET are not configured");
const playlistId = String(args.playlist_id || "");
const tracks = Array.isArray(args.tracks) ? (args.tracks as string[]) : [];
if (!playlistId || tracks.length === 0) throw new Error("playlist_id and tracks are required");
return client.addTracksToPlaylist(playlistId, tracks);
},
},
};
}