Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions src/albums.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MaxInt } from '@spotify/web-api-ts-sdk';
import { z } from 'zod';
import type { SpotifyHandlerExtra, tool } from './types.js';
import { formatDuration, handleSpotifyRequest } from './utils.js';
import { createSpotifyApi, formatDuration, handleSpotifyRequest, loadSpotifyConfig } from './utils.js';

const getAlbums: tool<{
albumIds: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString>]>;
Expand Down Expand Up @@ -204,11 +204,24 @@ const saveOrRemoveAlbumForUser: tool<{
}

try {
await handleSpotifyRequest(async (spotifyApi) => {
return action === 'save'
? await spotifyApi.currentUser.albums.saveAlbums(albumIds)
: await spotifyApi.currentUser.albums.removeSavedAlbums(albumIds);
});
await createSpotifyApi();
const config = loadSpotifyConfig();

const uris = albumIds.map((id) => `spotify:album:${id}`).join(',');
const response = await fetch(
`https://api.spotify.com/v1/me/library?uris=${encodeURIComponent(uris)}`,
{
method: action === 'save' ? 'PUT' : 'DELETE',
headers: {
Authorization: `Bearer ${config.accessToken}`,
},
},
);

if (!response.ok) {
const errorData = await response.text();
throw new Error(`Spotify API error ${response.status}: ${errorData}`);
}

const actionPastTense = action === 'save' ? 'saved' : 'removed';
const preposition = action === 'save' ? 'to' : 'from';
Expand Down
84 changes: 61 additions & 23 deletions src/play.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';
import type { SpotifyHandlerExtra, tool } from './types.js';
import { handleSpotifyRequest } from './utils.js';
import { createSpotifyApi, handleSpotifyRequest, loadSpotifyConfig } from './utils.js';

const playMusic: tool<{
uri: z.ZodOptional<z.ZodString>;
Expand Down Expand Up @@ -182,24 +182,47 @@ const createPlaylist: tool<{
handler: async (args, _extra: SpotifyHandlerExtra) => {
const { name, description, public: isPublic = false } = args;

const result = await handleSpotifyRequest(async (spotifyApi) => {
const me = await spotifyApi.currentUser.profile();
try {
await createSpotifyApi();
const config = loadSpotifyConfig();

return await spotifyApi.playlists.createPlaylist(me.id, {
name,
description,
public: isPublic,
});
});
const body: Record<string, string | boolean> = { name, public: isPublic };
if (description) body.description = description;

return {
content: [
{
type: 'text',
text: `Successfully created playlist "${name}"\nPlaylist ID: ${result.id}\nPlaylist URL: ${result.external_urls.spotify}`,
const response = await fetch('https://api.spotify.com/v1/me/playlists', {
method: 'POST',
headers: {
Authorization: `Bearer ${config.accessToken}`,
'Content-Type': 'application/json',
},
],
};
body: JSON.stringify(body),
});

if (!response.ok) {
const errorData = await response.text();
throw new Error(`Spotify API error ${response.status}: ${errorData}`);
}

const result = await response.json();

return {
content: [
{
type: 'text',
text: `Successfully created playlist "${name}"\nPlaylist ID: ${result.id}\nPlaylist URL: ${result.external_urls?.spotify ?? ''}`,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error creating playlist: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
},
};

Expand Down Expand Up @@ -236,13 +259,28 @@ const addTracksToPlaylist: tool<{
try {
const trackUris = trackIds.map((id) => `spotify:track:${id}`);

await handleSpotifyRequest(async (spotifyApi) => {
await spotifyApi.playlists.addItemsToPlaylist(
playlistId,
trackUris,
position,
);
});
await createSpotifyApi();
const config = loadSpotifyConfig();

const body: Record<string, unknown> = { uris: trackUris };
if (position !== undefined) body.position = position;

const response = await fetch(
`https://api.spotify.com/v1/playlists/${playlistId}/items`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${config.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);

if (!response.ok) {
const errorData = await response.text();
throw new Error(`Spotify API error ${response.status}: ${errorData}`);
}

return {
content: [
Expand Down
65 changes: 49 additions & 16 deletions src/playlist.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';
import type { SpotifyHandlerExtra, tool } from './types.js';
import { handleSpotifyRequest } from './utils.js';
import { createSpotifyApi, handleSpotifyRequest, loadSpotifyConfig } from './utils.js';

const getPlaylist: tool<{
playlistId: z.ZodString;
Expand Down Expand Up @@ -173,14 +173,29 @@ const removeTracksFromPlaylist: tool<{
const { playlistId, trackIds, snapshotId } = args;

try {
const tracks = trackIds.map((id) => ({ uri: `spotify:track:${id}` }));
await createSpotifyApi();
const config = loadSpotifyConfig();

await handleSpotifyRequest(async (spotifyApi) => {
await spotifyApi.playlists.removeItemsFromPlaylist(playlistId, {
tracks,
...(snapshotId ? { snapshot_id: snapshotId } : {}),
});
});
const items = trackIds.map((id) => ({ uri: `spotify:track:${id}` }));
const body: Record<string, unknown> = { tracks: items };
if (snapshotId) body.snapshot_id = snapshotId;

const response = await fetch(
`https://api.spotify.com/v1/playlists/${playlistId}/items`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${config.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);

if (!response.ok) {
const errorData = await response.text();
throw new Error(`Spotify API error ${response.status}: ${errorData}`);
}

return {
content: [
Expand Down Expand Up @@ -246,14 +261,32 @@ const reorderPlaylistItems: tool<{
args;

try {
await handleSpotifyRequest(async (spotifyApi) => {
await spotifyApi.playlists.updatePlaylistItems(playlistId, {
range_start: rangeStart,
insert_before: insertBefore,
...(rangeLength !== undefined ? { range_length: rangeLength } : {}),
...(snapshotId ? { snapshot_id: snapshotId } : {}),
});
});
await createSpotifyApi();
const config = loadSpotifyConfig();

const body: Record<string, unknown> = {
range_start: rangeStart,
insert_before: insertBefore,
};
if (rangeLength !== undefined) body.range_length = rangeLength;
if (snapshotId) body.snapshot_id = snapshotId;

const response = await fetch(
`https://api.spotify.com/v1/playlists/${playlistId}/items`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${config.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);

if (!response.ok) {
const errorData = await response.text();
throw new Error(`Spotify API error ${response.status}: ${errorData}`);
}

const count = rangeLength ?? 1;
return {
Expand Down