chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyAddPlaylistCoverParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
imageBase64: string
|
||||
}
|
||||
|
||||
interface SpotifyAddPlaylistCoverResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyAddPlaylistCoverTool: ToolConfig<
|
||||
SpotifyAddPlaylistCoverParams,
|
||||
SpotifyAddPlaylistCoverResponse
|
||||
> = {
|
||||
id: 'spotify_add_playlist_cover',
|
||||
name: 'Spotify Add Playlist Cover',
|
||||
description: 'Upload a custom cover image for a playlist. Image must be JPEG and under 256KB.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private', 'ugc-image-upload'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
imageBase64: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Base64-encoded JPEG image data (max 256KB)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/images`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'image/jpeg',
|
||||
}),
|
||||
body: (params) => params.imageBase64,
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyAddPlaylistCoverResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether upload succeeded' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { SpotifyAddToQueueParams, SpotifyAddToQueueResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyAddToQueueTool: ToolConfig<SpotifyAddToQueueParams, SpotifyAddToQueueResponse> =
|
||||
{
|
||||
id: 'spotify_add_to_queue',
|
||||
name: 'Spotify Add to Queue',
|
||||
description: "Add a track to the user's playback queue.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Spotify URI of the track to add (e.g., "spotify:track:xxx")',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID. If not provided, uses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/me/player/queue?uri=${encodeURIComponent(params.uri)}`
|
||||
if (params.device_id) {
|
||||
url += `&device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyAddToQueueResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether track was added to queue' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
SpotifyAddTracksToPlaylistParams,
|
||||
SpotifyAddTracksToPlaylistResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyAddTracksToPlaylistTool: ToolConfig<
|
||||
SpotifyAddTracksToPlaylistParams,
|
||||
SpotifyAddTracksToPlaylistResponse
|
||||
> = {
|
||||
id: 'spotify_add_tracks_to_playlist',
|
||||
name: 'Spotify Add Tracks to Playlist',
|
||||
description: 'Add one or more tracks to a Spotify playlist.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the playlist',
|
||||
},
|
||||
uris: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated Spotify URIs (e.g., "spotify:track:xxx,spotify:track:yyy")',
|
||||
},
|
||||
position: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position to insert tracks (0-based index). If omitted, tracks are appended.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const uris = params.uris.split(',').map((uri) => uri.trim())
|
||||
const body: any = { uris }
|
||||
if (params.position !== undefined) {
|
||||
body.position = params.position
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyAddTracksToPlaylistResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
snapshot_id: data.snapshot_id,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID after modification' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyCheckFollowingParams {
|
||||
accessToken: string
|
||||
type: string
|
||||
ids: string
|
||||
}
|
||||
|
||||
interface SpotifyCheckFollowingResponse extends ToolResponse {
|
||||
output: { results: boolean[] }
|
||||
}
|
||||
|
||||
export const spotifyCheckFollowingTool: ToolConfig<
|
||||
SpotifyCheckFollowingParams,
|
||||
SpotifyCheckFollowingResponse
|
||||
> = {
|
||||
id: 'spotify_check_following',
|
||||
name: 'Spotify Check Following',
|
||||
description: 'Check if the user follows artists or users.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-follow-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
type: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Type to check: "artist" or "user"',
|
||||
},
|
||||
ids: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated artist or user IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.ids
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/following/contains?type=${params.type}&ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyCheckFollowingResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each ID' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyCheckPlaylistFollowersParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
userIds: string
|
||||
}
|
||||
|
||||
interface SpotifyCheckPlaylistFollowersResponse extends ToolResponse {
|
||||
output: { results: boolean[] }
|
||||
}
|
||||
|
||||
export const spotifyCheckPlaylistFollowersTool: ToolConfig<
|
||||
SpotifyCheckPlaylistFollowersParams,
|
||||
SpotifyCheckPlaylistFollowersResponse
|
||||
> = {
|
||||
id: 'spotify_check_playlist_followers',
|
||||
name: 'Spotify Check Playlist Followers',
|
||||
description: 'Check if users follow a playlist.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
userIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated user IDs to check (max 5)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.userIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 5)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/playlists/${params.playlistId}/followers/contains?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyCheckPlaylistFollowersResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each user' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyCheckSavedAlbumsParams {
|
||||
accessToken: string
|
||||
albumIds: string
|
||||
}
|
||||
|
||||
interface SpotifyCheckSavedAlbumsResponse extends ToolResponse {
|
||||
output: { results: boolean[] }
|
||||
}
|
||||
|
||||
export const spotifyCheckSavedAlbumsTool: ToolConfig<
|
||||
SpotifyCheckSavedAlbumsParams,
|
||||
SpotifyCheckSavedAlbumsResponse
|
||||
> = {
|
||||
id: 'spotify_check_saved_albums',
|
||||
name: 'Spotify Check Saved Albums',
|
||||
description: 'Check if albums are saved in library.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
albumIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated album IDs (max 20)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.albumIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 20)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/albums/contains?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyCheckSavedAlbumsResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each album' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyCheckSavedAudiobooksParams {
|
||||
accessToken: string
|
||||
audiobookIds: string
|
||||
}
|
||||
|
||||
interface SpotifyCheckSavedAudiobooksResponse extends ToolResponse {
|
||||
output: { results: boolean[] }
|
||||
}
|
||||
|
||||
export const spotifyCheckSavedAudiobooksTool: ToolConfig<
|
||||
SpotifyCheckSavedAudiobooksParams,
|
||||
SpotifyCheckSavedAudiobooksResponse
|
||||
> = {
|
||||
id: 'spotify_check_saved_audiobooks',
|
||||
name: 'Spotify Check Saved Audiobooks',
|
||||
description: 'Check if audiobooks are saved in library.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
audiobookIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated audiobook IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.audiobookIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/audiobooks/contains?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyCheckSavedAudiobooksResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each audiobook' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyCheckSavedEpisodesParams {
|
||||
accessToken: string
|
||||
episodeIds: string
|
||||
}
|
||||
|
||||
interface SpotifyCheckSavedEpisodesResponse extends ToolResponse {
|
||||
output: { results: boolean[] }
|
||||
}
|
||||
|
||||
export const spotifyCheckSavedEpisodesTool: ToolConfig<
|
||||
SpotifyCheckSavedEpisodesParams,
|
||||
SpotifyCheckSavedEpisodesResponse
|
||||
> = {
|
||||
id: 'spotify_check_saved_episodes',
|
||||
name: 'Spotify Check Saved Episodes',
|
||||
description: 'Check if episodes are saved in library.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
episodeIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated episode IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.episodeIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/episodes/contains?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyCheckSavedEpisodesResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each episode' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyCheckSavedShowsParams {
|
||||
accessToken: string
|
||||
showIds: string
|
||||
}
|
||||
|
||||
interface SpotifyCheckSavedShowsResponse extends ToolResponse {
|
||||
output: { results: boolean[] }
|
||||
}
|
||||
|
||||
export const spotifyCheckSavedShowsTool: ToolConfig<
|
||||
SpotifyCheckSavedShowsParams,
|
||||
SpotifyCheckSavedShowsResponse
|
||||
> = {
|
||||
id: 'spotify_check_saved_shows',
|
||||
name: 'Spotify Check Saved Shows',
|
||||
description: 'Check if shows are saved in library.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated show IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.showIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/shows/contains?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyCheckSavedShowsResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each show' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
SpotifyCheckSavedTracksParams,
|
||||
SpotifyCheckSavedTracksResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyCheckSavedTracksTool: ToolConfig<
|
||||
SpotifyCheckSavedTracksParams,
|
||||
SpotifyCheckSavedTracksResponse
|
||||
> = {
|
||||
id: 'spotify_check_saved_tracks',
|
||||
name: 'Spotify Check Saved Tracks',
|
||||
description: "Check if one or more tracks are saved in the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
trackIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated track IDs to check (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.trackIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/tracks/contains?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params): Promise<SpotifyCheckSavedTracksResponse> => {
|
||||
const data = await response.json()
|
||||
const ids = (params?.trackIds || '')
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
|
||||
const results = ids.map((id, index) => ({
|
||||
id,
|
||||
saved: data[index] || false,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
all_saved: data.every((saved: boolean) => saved),
|
||||
none_saved: data.every((saved: boolean) => !saved),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of track IDs with saved status' },
|
||||
all_saved: { type: 'boolean', description: 'Whether all tracks are saved' },
|
||||
none_saved: { type: 'boolean', description: 'Whether no tracks are saved' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
SpotifyCreatePlaylistParams,
|
||||
SpotifyCreatePlaylistResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyCreatePlaylistTool: ToolConfig<
|
||||
SpotifyCreatePlaylistParams,
|
||||
SpotifyCreatePlaylistResponse
|
||||
> = {
|
||||
id: 'spotify_create_playlist',
|
||||
name: 'Spotify Create Playlist',
|
||||
description: 'Create a new playlist for the current user on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name for the new playlist',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Description for the playlist',
|
||||
},
|
||||
public: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: true,
|
||||
description: 'Whether the playlist should be public',
|
||||
},
|
||||
collaborative: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: false,
|
||||
description: 'Whether the playlist should be collaborative (requires public to be false)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/playlists',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
name: params.name,
|
||||
description: params.description || '',
|
||||
public: params.public !== false,
|
||||
collaborative: params.collaborative === true,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyCreatePlaylistResponse> => {
|
||||
const playlist = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
public: playlist.public,
|
||||
collaborative: playlist.collaborative,
|
||||
snapshot_id: playlist.snapshot_id,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify playlist ID' },
|
||||
name: { type: 'string', description: 'Playlist name' },
|
||||
description: { type: 'string', description: 'Playlist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether the playlist is public' },
|
||||
collaborative: { type: 'boolean', description: 'Whether collaborative' },
|
||||
snapshot_id: { type: 'string', description: 'Playlist snapshot ID' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyFollowArtistsParams {
|
||||
accessToken: string
|
||||
artistIds: string
|
||||
}
|
||||
|
||||
interface SpotifyFollowArtistsResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyFollowArtistsTool: ToolConfig<
|
||||
SpotifyFollowArtistsParams,
|
||||
SpotifyFollowArtistsResponse
|
||||
> = {
|
||||
id: 'spotify_follow_artists',
|
||||
name: 'Spotify Follow Artists',
|
||||
description: 'Follow one or more artists.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-follow-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
artistIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated artist IDs to follow (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.artistIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/following?type=artist&ids=${ids}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyFollowArtistsResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether artists were followed successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyFollowPlaylistParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
public?: boolean
|
||||
}
|
||||
|
||||
interface SpotifyFollowPlaylistResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyFollowPlaylistTool: ToolConfig<
|
||||
SpotifyFollowPlaylistParams,
|
||||
SpotifyFollowPlaylistResponse
|
||||
> = {
|
||||
id: 'spotify_follow_playlist',
|
||||
name: 'Spotify Follow Playlist',
|
||||
description: 'Follow (save) a playlist.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
public: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: true,
|
||||
description: 'Whether the playlist will appear in public playlists',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/followers`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
public: params.public ?? true,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyFollowPlaylistResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether follow succeeded' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { SpotifyGetAlbumParams, SpotifyGetAlbumResponse } from '@/tools/spotify/types'
|
||||
import {
|
||||
SIMPLIFIED_ALBUM_TRACK_OUTPUT_PROPERTIES,
|
||||
SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetAlbumTool: ToolConfig<SpotifyGetAlbumParams, SpotifyGetAlbumResponse> = {
|
||||
id: 'spotify_get_album',
|
||||
name: 'Spotify Get Album',
|
||||
description:
|
||||
'Get detailed information about an album on Spotify by its ID, including track listing.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
albumId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the album',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code for track availability (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/albums/${params.albumId}`
|
||||
if (params.market) {
|
||||
url += `?market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetAlbumResponse> => {
|
||||
const album = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album_type: album.album_type,
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
label: album.label || '',
|
||||
popularity: album.popularity,
|
||||
genres: album.genres || [],
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
tracks: (album.tracks?.items || []).map((t: any) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
duration_ms: t.duration_ms,
|
||||
track_number: t.track_number,
|
||||
})),
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify album ID' },
|
||||
name: { type: 'string', description: 'Album name' },
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of artists',
|
||||
items: { type: 'object', properties: SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
album_type: { type: 'string', description: 'Type of album (album, single, compilation)' },
|
||||
total_tracks: { type: 'number', description: 'Total number of tracks' },
|
||||
release_date: { type: 'string', description: 'Release date' },
|
||||
label: { type: 'string', description: 'Record label' },
|
||||
popularity: { type: 'number', description: 'Popularity score (0-100)' },
|
||||
genres: { type: 'array', description: 'List of genres' },
|
||||
image_url: { type: 'string', description: 'Album cover image URL', optional: true },
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of tracks on the album',
|
||||
items: { type: 'object', properties: SIMPLIFIED_ALBUM_TRACK_OUTPUT_PROPERTIES },
|
||||
},
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { ALBUM_TRACK_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetAlbumTracksParams {
|
||||
accessToken: string
|
||||
albumId: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetAlbumTracksResponse extends ToolResponse {
|
||||
output: {
|
||||
tracks: Array<{
|
||||
id: string
|
||||
name: string
|
||||
artists: Array<{ id: string; name: string }>
|
||||
duration_ms: number
|
||||
track_number: number
|
||||
disc_number: number
|
||||
explicit: boolean
|
||||
preview_url: string | null
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetAlbumTracksTool: ToolConfig<
|
||||
SpotifyGetAlbumTracksParams,
|
||||
SpotifyGetAlbumTracksResponse
|
||||
> = {
|
||||
id: 'spotify_get_album_tracks',
|
||||
name: 'Spotify Get Album Tracks',
|
||||
description: 'Get the tracks from an album.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
albumId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify album ID',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of tracks to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first track to return for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/albums/${params.albumId}/tracks?limit=${limit}&offset=${offset}`
|
||||
if (params.market) {
|
||||
url += `&market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetAlbumTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
duration_ms: track.duration_ms,
|
||||
track_number: track.track_number,
|
||||
disc_number: track.disc_number,
|
||||
explicit: track.explicit || false,
|
||||
preview_url: track.preview_url || null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of tracks',
|
||||
items: { type: 'object', properties: ALBUM_TRACK_OUTPUT_PROPERTIES },
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of tracks' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetAlbumsParams {
|
||||
accessToken: string
|
||||
albumIds: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetAlbumsResponse extends ToolResponse {
|
||||
output: {
|
||||
albums: Array<{
|
||||
id: string
|
||||
name: string
|
||||
artists: Array<{ id: string; name: string }>
|
||||
album_type: string
|
||||
total_tracks: number
|
||||
release_date: string
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetAlbumsTool: ToolConfig<SpotifyGetAlbumsParams, SpotifyGetAlbumsResponse> = {
|
||||
id: 'spotify_get_albums',
|
||||
name: 'Spotify Get Multiple Albums',
|
||||
description: 'Get details for multiple albums by their IDs.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
albumIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated album IDs (max 20)',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.albumIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 20)
|
||||
.join(',')
|
||||
let url = `https://api.spotify.com/v1/albums?ids=${ids}`
|
||||
if (params.market) {
|
||||
url += `&market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetAlbumsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const albums = (data.albums || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album_type: album.album_type,
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { albums },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: {
|
||||
type: 'array',
|
||||
description: 'List of albums',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify album ID' },
|
||||
name: { type: 'string', description: 'Album name' },
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of artists',
|
||||
items: { type: 'object', properties: SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
album_type: { type: 'string', description: 'Type of album (album, single, compilation)' },
|
||||
total_tracks: { type: 'number', description: 'Total number of tracks' },
|
||||
release_date: { type: 'string', description: 'Release date' },
|
||||
image_url: { type: 'string', description: 'Album cover image URL', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { SpotifyGetArtistParams, SpotifyGetArtistResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetArtistTool: ToolConfig<SpotifyGetArtistParams, SpotifyGetArtistResponse> = {
|
||||
id: 'spotify_get_artist',
|
||||
name: 'Spotify Get Artist',
|
||||
description: 'Get detailed information about an artist on Spotify by their ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
artistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the artist',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/artists/${params.artistId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetArtistResponse> => {
|
||||
const artist = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify artist ID' },
|
||||
name: { type: 'string', description: 'Artist name' },
|
||||
genres: { type: 'array', description: 'List of genres associated with the artist' },
|
||||
popularity: { type: 'number', description: 'Popularity score (0-100)' },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Artist image URL', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
SpotifyGetArtistAlbumsParams,
|
||||
SpotifyGetArtistAlbumsResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetArtistAlbumsTool: ToolConfig<
|
||||
SpotifyGetArtistAlbumsParams,
|
||||
SpotifyGetArtistAlbumsResponse
|
||||
> = {
|
||||
id: 'spotify_get_artist_albums',
|
||||
name: 'Spotify Get Artist Albums',
|
||||
description: 'Get albums by an artist on Spotify. Can filter by album type.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
artistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the artist',
|
||||
},
|
||||
include_groups: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by album type: album, single, appears_on, compilation (comma-separated)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Maximum number of albums to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of the first album to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/artists/${params.artistId}/albums?limit=${limit}&offset=${offset}`
|
||||
if (params.include_groups) {
|
||||
url += `&include_groups=${encodeURIComponent(params.include_groups)}`
|
||||
}
|
||||
if (params.market) {
|
||||
url += `&market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetArtistAlbumsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const albums = (data.items || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
album_type: album.album_type,
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
albums,
|
||||
total: data.total || albums.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: {
|
||||
type: 'array',
|
||||
description: "Artist's albums",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify album ID' },
|
||||
name: { type: 'string', description: 'Album name' },
|
||||
album_type: { type: 'string', description: 'Type (album, single, compilation)' },
|
||||
total_tracks: { type: 'number', description: 'Number of tracks' },
|
||||
release_date: { type: 'string', description: 'Release date' },
|
||||
image_url: { type: 'string', description: 'Album cover URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of albums available' },
|
||||
next: { type: 'string', description: 'URL for next page of results', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type {
|
||||
SpotifyGetArtistTopTracksParams,
|
||||
SpotifyGetArtistTopTracksResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import { ARTIST_TOP_TRACK_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetArtistTopTracksTool: ToolConfig<
|
||||
SpotifyGetArtistTopTracksParams,
|
||||
SpotifyGetArtistTopTracksResponse
|
||||
> = {
|
||||
id: 'spotify_get_artist_top_tracks',
|
||||
name: 'Spotify Get Artist Top Tracks',
|
||||
description: 'Get the top 10 most popular tracks by an artist on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
artistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the artist',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 'US',
|
||||
description:
|
||||
'ISO 3166-1 alpha-2 country code (e.g., "US", "GB") - required for this endpoint',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const market = params.market || 'US'
|
||||
return `https://api.spotify.com/v1/artists/${params.artistId}/top-tracks?market=${market}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetArtistTopTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.tracks || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: "Artist's top tracks",
|
||||
items: { type: 'object', properties: ARTIST_TOP_TRACK_OUTPUT_PROPERTIES },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { ARTIST_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetArtistsParams {
|
||||
accessToken: string
|
||||
artistIds: string
|
||||
}
|
||||
|
||||
interface SpotifyGetArtistsResponse extends ToolResponse {
|
||||
output: {
|
||||
artists: Array<{
|
||||
id: string
|
||||
name: string
|
||||
genres: string[]
|
||||
popularity: number
|
||||
followers: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetArtistsTool: ToolConfig<SpotifyGetArtistsParams, SpotifyGetArtistsResponse> =
|
||||
{
|
||||
id: 'spotify_get_artists',
|
||||
name: 'Spotify Get Multiple Artists',
|
||||
description: 'Get details for multiple artists by their IDs.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
artistIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated artist IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.artistIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/artists?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetArtistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const artists = (data.artists || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity || 0,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { artists },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of artists',
|
||||
items: { type: 'object', properties: ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetAudiobookParams {
|
||||
accessToken: string
|
||||
audiobookId: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetAudiobookResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
authors: Array<{ name: string }>
|
||||
narrators: Array<{ name: string }>
|
||||
publisher: string
|
||||
description: string
|
||||
total_chapters: number
|
||||
languages: string[]
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetAudiobookTool: ToolConfig<
|
||||
SpotifyGetAudiobookParams,
|
||||
SpotifyGetAudiobookResponse
|
||||
> = {
|
||||
id: 'spotify_get_audiobook',
|
||||
name: 'Spotify Get Audiobook',
|
||||
description: 'Get details for an audiobook.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
audiobookId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify audiobook ID',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/audiobooks/${params.audiobookId}`
|
||||
if (params.market) url += `?market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetAudiobookResponse> => {
|
||||
const book = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: book.id,
|
||||
name: book.name,
|
||||
authors: book.authors || [],
|
||||
narrators: book.narrators || [],
|
||||
publisher: book.publisher || '',
|
||||
description: book.description || '',
|
||||
total_chapters: book.total_chapters || 0,
|
||||
languages: book.languages || [],
|
||||
image_url: book.images?.[0]?.url || null,
|
||||
external_url: book.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Audiobook ID' },
|
||||
name: { type: 'string', description: 'Audiobook name' },
|
||||
authors: { type: 'json', description: 'Authors' },
|
||||
narrators: { type: 'json', description: 'Narrators' },
|
||||
publisher: { type: 'string', description: 'Publisher' },
|
||||
description: { type: 'string', description: 'Description' },
|
||||
total_chapters: { type: 'number', description: 'Total chapters' },
|
||||
languages: { type: 'json', description: 'Languages' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetAudiobookChaptersParams {
|
||||
accessToken: string
|
||||
audiobookId: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetAudiobookChaptersResponse extends ToolResponse {
|
||||
output: {
|
||||
chapters: Array<{
|
||||
id: string
|
||||
name: string
|
||||
chapter_number: number
|
||||
duration_ms: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetAudiobookChaptersTool: ToolConfig<
|
||||
SpotifyGetAudiobookChaptersParams,
|
||||
SpotifyGetAudiobookChaptersResponse
|
||||
> = {
|
||||
id: 'spotify_get_audiobook_chapters',
|
||||
name: 'Spotify Get Audiobook Chapters',
|
||||
description: 'Get chapters from an audiobook.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
audiobookId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify audiobook ID',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of chapters to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first chapter to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/audiobooks/${params.audiobookId}/chapters?limit=${limit}&offset=${offset}`
|
||||
if (params.market) url += `&market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetAudiobookChaptersResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
chapters: (data.items || []).map((ch: any) => ({
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
chapter_number: ch.chapter_number || 0,
|
||||
duration_ms: ch.duration_ms || 0,
|
||||
image_url: ch.images?.[0]?.url || null,
|
||||
external_url: ch.external_urls?.spotify || '',
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
chapters: { type: 'json', description: 'List of chapters' },
|
||||
total: { type: 'number', description: 'Total chapters' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetAudiobooksParams {
|
||||
accessToken: string
|
||||
audiobookIds: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetAudiobooksResponse extends ToolResponse {
|
||||
output: {
|
||||
audiobooks: Array<{
|
||||
id: string
|
||||
name: string
|
||||
authors: Array<{ name: string }>
|
||||
total_chapters: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetAudiobooksTool: ToolConfig<
|
||||
SpotifyGetAudiobooksParams,
|
||||
SpotifyGetAudiobooksResponse
|
||||
> = {
|
||||
id: 'spotify_get_audiobooks',
|
||||
name: 'Spotify Get Multiple Audiobooks',
|
||||
description: 'Get details for multiple audiobooks.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
audiobookIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated audiobook IDs (max 50)',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.audiobookIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
let url = `https://api.spotify.com/v1/audiobooks?ids=${ids}`
|
||||
if (params.market) url += `&market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetAudiobooksResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
audiobooks: (data.audiobooks || []).map((book: any) => ({
|
||||
id: book.id,
|
||||
name: book.name,
|
||||
authors: book.authors || [],
|
||||
total_chapters: book.total_chapters || 0,
|
||||
image_url: book.images?.[0]?.url || null,
|
||||
external_url: book.external_urls?.spotify || '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
audiobooks: { type: 'json', description: 'List of audiobooks' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type {
|
||||
SpotifyGetCategoriesParams,
|
||||
SpotifyGetCategoriesResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetCategoriesTool: ToolConfig<
|
||||
SpotifyGetCategoriesParams,
|
||||
SpotifyGetCategoriesResponse
|
||||
> = {
|
||||
id: 'spotify_get_categories',
|
||||
name: 'Spotify Get Categories',
|
||||
description: 'Get a list of browse categories used to tag items in Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Locale code (e.g., "en_US", "es_MX")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of categories to return (1-50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
let url = `https://api.spotify.com/v1/browse/categories?limit=${limit}`
|
||||
if (params.country) {
|
||||
url += `&country=${params.country}`
|
||||
}
|
||||
if (params.locale) {
|
||||
url += `&locale=${params.locale}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetCategoriesResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const categories = (data.categories?.items || []).map((category: any) => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
icon_url: category.icons?.[0]?.url || null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
categories,
|
||||
total: data.categories?.total || 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
categories: { type: 'json', description: 'List of browse categories' },
|
||||
total: { type: 'number', description: 'Total number of categories' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type {
|
||||
SpotifyGetCurrentUserParams,
|
||||
SpotifyGetCurrentUserResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetCurrentUserTool: ToolConfig<
|
||||
SpotifyGetCurrentUserParams,
|
||||
SpotifyGetCurrentUserResponse
|
||||
> = {
|
||||
id: 'spotify_get_current_user',
|
||||
name: 'Spotify Get Current User',
|
||||
description: "Get the current user's Spotify profile information.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private', 'user-read-email'],
|
||||
},
|
||||
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetCurrentUserResponse> => {
|
||||
const user = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: user.id,
|
||||
display_name: user.display_name || '',
|
||||
email: user.email || null,
|
||||
country: user.country || null,
|
||||
product: user.product || null,
|
||||
followers: user.followers?.total || 0,
|
||||
image_url: user.images?.[0]?.url || null,
|
||||
external_url: user.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify user ID' },
|
||||
display_name: { type: 'string', description: 'Display name' },
|
||||
email: { type: 'string', description: 'Email address', optional: true },
|
||||
country: { type: 'string', description: 'Country code', optional: true },
|
||||
product: { type: 'string', description: 'Subscription level (free, premium)', optional: true },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Profile image URL', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify profile URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { CURRENTLY_PLAYING_TRACK_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetCurrentlyPlayingParams {
|
||||
accessToken: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetCurrentlyPlayingResponse extends ToolResponse {
|
||||
output: {
|
||||
is_playing: boolean
|
||||
progress_ms: number | null
|
||||
track: {
|
||||
id: string
|
||||
name: string
|
||||
artists: Array<{ id: string; name: string }>
|
||||
album: {
|
||||
id: string
|
||||
name: string
|
||||
image_url: string | null
|
||||
}
|
||||
duration_ms: number
|
||||
external_url: string
|
||||
} | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetCurrentlyPlayingTool: ToolConfig<
|
||||
SpotifyGetCurrentlyPlayingParams,
|
||||
SpotifyGetCurrentlyPlayingResponse
|
||||
> = {
|
||||
id: 'spotify_get_currently_playing',
|
||||
name: 'Spotify Get Currently Playing',
|
||||
description: "Get the user's currently playing track.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-currently-playing'],
|
||||
},
|
||||
|
||||
params: {
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player/currently-playing'
|
||||
if (params.market) {
|
||||
url += `?market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetCurrentlyPlayingResponse> => {
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: false,
|
||||
progress_ms: null,
|
||||
track: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: data.is_playing || false,
|
||||
progress_ms: data.progress_ms || null,
|
||||
track: data.item
|
||||
? {
|
||||
id: data.item.id,
|
||||
name: data.item.name,
|
||||
artists: data.item.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: data.item.album?.id || '',
|
||||
name: data.item.album?.name || '',
|
||||
image_url: data.item.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: data.item.duration_ms,
|
||||
external_url: data.item.external_urls?.spotify || '',
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
is_playing: { type: 'boolean', description: 'Whether playback is active' },
|
||||
progress_ms: { type: 'number', description: 'Current position in track (ms)', optional: true },
|
||||
track: {
|
||||
type: 'object',
|
||||
description: 'Currently playing track',
|
||||
optional: true,
|
||||
properties: CURRENTLY_PLAYING_TRACK_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { SpotifyGetDevicesParams, SpotifyGetDevicesResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetDevicesTool: ToolConfig<SpotifyGetDevicesParams, SpotifyGetDevicesResponse> =
|
||||
{
|
||||
id: 'spotify_get_devices',
|
||||
name: 'Spotify Get Devices',
|
||||
description: "Get the user's available Spotify playback devices.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-state'],
|
||||
},
|
||||
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/player/devices',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetDevicesResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const devices = (data.devices || []).map((device: any) => ({
|
||||
id: device.id,
|
||||
is_active: device.is_active,
|
||||
is_private_session: device.is_private_session,
|
||||
is_restricted: device.is_restricted,
|
||||
name: device.name,
|
||||
type: device.type,
|
||||
volume_percent: device.volume_percent,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
devices,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
devices: {
|
||||
type: 'array',
|
||||
description: 'Available playback devices',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Device ID' },
|
||||
is_active: { type: 'boolean', description: 'Whether device is active' },
|
||||
is_private_session: { type: 'boolean', description: 'Whether in private session' },
|
||||
is_restricted: { type: 'boolean', description: 'Whether device is restricted' },
|
||||
name: { type: 'string', description: 'Device name' },
|
||||
type: { type: 'string', description: 'Device type (Computer, Smartphone, etc.)' },
|
||||
volume_percent: { type: 'number', description: 'Current volume (0-100)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetEpisodeParams {
|
||||
accessToken: string
|
||||
episodeId: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetEpisodeResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
duration_ms: number
|
||||
release_date: string
|
||||
explicit: boolean
|
||||
show: { id: string; name: string; publisher: string }
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetEpisodeTool: ToolConfig<SpotifyGetEpisodeParams, SpotifyGetEpisodeResponse> =
|
||||
{
|
||||
id: 'spotify_get_episode',
|
||||
name: 'Spotify Get Episode',
|
||||
description: 'Get details for a podcast episode.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
episodeId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify episode ID',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/episodes/${params.episodeId}`
|
||||
if (params.market) url += `?market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetEpisodeResponse> => {
|
||||
const ep = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: ep.id,
|
||||
name: ep.name,
|
||||
description: ep.description || '',
|
||||
duration_ms: ep.duration_ms || 0,
|
||||
release_date: ep.release_date || '',
|
||||
explicit: ep.explicit || false,
|
||||
show: {
|
||||
id: ep.show?.id || '',
|
||||
name: ep.show?.name || '',
|
||||
publisher: ep.show?.publisher || '',
|
||||
},
|
||||
image_url: ep.images?.[0]?.url || null,
|
||||
external_url: ep.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Episode ID' },
|
||||
name: { type: 'string', description: 'Episode name' },
|
||||
description: { type: 'string', description: 'Episode description' },
|
||||
duration_ms: { type: 'number', description: 'Duration in ms' },
|
||||
release_date: { type: 'string', description: 'Release date' },
|
||||
explicit: { type: 'boolean', description: 'Contains explicit content' },
|
||||
show: { type: 'json', description: 'Parent show info' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetEpisodesParams {
|
||||
accessToken: string
|
||||
episodeIds: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetEpisodesResponse extends ToolResponse {
|
||||
output: {
|
||||
episodes: Array<{
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
duration_ms: number
|
||||
release_date: string
|
||||
show: { id: string; name: string }
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetEpisodesTool: ToolConfig<
|
||||
SpotifyGetEpisodesParams,
|
||||
SpotifyGetEpisodesResponse
|
||||
> = {
|
||||
id: 'spotify_get_episodes',
|
||||
name: 'Spotify Get Multiple Episodes',
|
||||
description: 'Get details for multiple podcast episodes.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
episodeIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated episode IDs (max 50)',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.episodeIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
let url = `https://api.spotify.com/v1/episodes?ids=${ids}`
|
||||
if (params.market) url += `&market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetEpisodesResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
episodes: (data.episodes || []).map((ep: any) => ({
|
||||
id: ep.id,
|
||||
name: ep.name,
|
||||
description: ep.description || '',
|
||||
duration_ms: ep.duration_ms || 0,
|
||||
release_date: ep.release_date || '',
|
||||
show: { id: ep.show?.id || '', name: ep.show?.name || '' },
|
||||
image_url: ep.images?.[0]?.url || null,
|
||||
external_url: ep.external_urls?.spotify || '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
episodes: { type: 'json', description: 'List of episodes' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { ARTIST_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetFollowedArtistsParams {
|
||||
accessToken: string
|
||||
limit?: number
|
||||
after?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetFollowedArtistsResponse extends ToolResponse {
|
||||
output: {
|
||||
artists: Array<{
|
||||
id: string
|
||||
name: string
|
||||
genres: string[]
|
||||
popularity: number
|
||||
followers: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetFollowedArtistsTool: ToolConfig<
|
||||
SpotifyGetFollowedArtistsParams,
|
||||
SpotifyGetFollowedArtistsResponse
|
||||
> = {
|
||||
id: 'spotify_get_followed_artists',
|
||||
name: 'Spotify Get Followed Artists',
|
||||
description: "Get the user's followed artists.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-follow-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of artists to return (1-50)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor for pagination (last artist ID from previous request)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
let url = `https://api.spotify.com/v1/me/following?type=artist&limit=${limit}`
|
||||
if (params.after) {
|
||||
url += `&after=${params.after}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetFollowedArtistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const artists = (data.artists?.items || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity || 0,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
artists,
|
||||
total: data.artists?.total || 0,
|
||||
next: data.artists?.cursors?.after || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of followed artists',
|
||||
items: { type: 'object', properties: ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of followed artists' },
|
||||
next: { type: 'string', description: 'Cursor for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetMarketsParams {
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
interface SpotifyGetMarketsResponse extends ToolResponse {
|
||||
output: {
|
||||
markets: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetMarketsTool: ToolConfig<SpotifyGetMarketsParams, SpotifyGetMarketsResponse> =
|
||||
{
|
||||
id: 'spotify_get_markets',
|
||||
name: 'Spotify Get Available Markets',
|
||||
description: 'Get the list of markets where Spotify is available.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/markets',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetMarketsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: { markets: data.markets || [] },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
markets: { type: 'json', description: 'List of ISO country codes' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type {
|
||||
SpotifyGetNewReleasesParams,
|
||||
SpotifyGetNewReleasesResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import { ALBUM_WITH_ARTISTS_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetNewReleasesTool: ToolConfig<
|
||||
SpotifyGetNewReleasesParams,
|
||||
SpotifyGetNewReleasesResponse
|
||||
> = {
|
||||
id: 'spotify_get_new_releases',
|
||||
name: 'Spotify Get New Releases',
|
||||
description: 'Get a list of new album releases featured in Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of releases to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first release to return for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/browse/new-releases?limit=${limit}&offset=${offset}`
|
||||
if (params.country) {
|
||||
url += `&country=${params.country}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetNewReleasesResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const albums = (data.albums?.items || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
release_date: album.release_date,
|
||||
total_tracks: album.total_tracks,
|
||||
album_type: album.album_type,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
albums,
|
||||
total: data.albums?.total || 0,
|
||||
next: data.albums?.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: {
|
||||
type: 'array',
|
||||
description: 'List of new releases',
|
||||
items: { type: 'object', properties: ALBUM_WITH_ARTISTS_OUTPUT_PROPERTIES },
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of new releases' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
SpotifyGetPlaybackStateParams,
|
||||
SpotifyGetPlaybackStateResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import {
|
||||
PLAYBACK_TRACK_OUTPUT_PROPERTIES,
|
||||
SIMPLIFIED_DEVICE_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetPlaybackStateTool: ToolConfig<
|
||||
SpotifyGetPlaybackStateParams,
|
||||
SpotifyGetPlaybackStateResponse
|
||||
> = {
|
||||
id: 'spotify_get_playback_state',
|
||||
name: 'Spotify Get Playback State',
|
||||
description: 'Get the current playback state including device, track, and progress.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player'
|
||||
if (params.market) {
|
||||
url += `?market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetPlaybackStateResponse> => {
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: false,
|
||||
device: null,
|
||||
progress_ms: null,
|
||||
currently_playing_type: 'unknown',
|
||||
shuffle_state: false,
|
||||
repeat_state: 'off',
|
||||
track: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: data.is_playing || false,
|
||||
device: data.device
|
||||
? {
|
||||
id: data.device.id,
|
||||
name: data.device.name,
|
||||
type: data.device.type,
|
||||
volume_percent: data.device.volume_percent,
|
||||
}
|
||||
: null,
|
||||
progress_ms: data.progress_ms,
|
||||
currently_playing_type: data.currently_playing_type || 'unknown',
|
||||
shuffle_state: data.shuffle_state || false,
|
||||
repeat_state: data.repeat_state || 'off',
|
||||
track: data.item
|
||||
? {
|
||||
id: data.item.id,
|
||||
name: data.item.name,
|
||||
artists: data.item.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: data.item.album?.id || '',
|
||||
name: data.item.album?.name || '',
|
||||
image_url: data.item.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: data.item.duration_ms,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
is_playing: { type: 'boolean', description: 'Whether playback is active' },
|
||||
device: {
|
||||
type: 'object',
|
||||
description: 'Active device information',
|
||||
optional: true,
|
||||
properties: SIMPLIFIED_DEVICE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
progress_ms: { type: 'number', description: 'Progress in milliseconds', optional: true },
|
||||
currently_playing_type: { type: 'string', description: 'Type of content playing' },
|
||||
shuffle_state: { type: 'boolean', description: 'Whether shuffle is enabled' },
|
||||
repeat_state: { type: 'string', description: 'Repeat mode (off, track, context)' },
|
||||
track: {
|
||||
type: 'object',
|
||||
description: 'Currently playing track',
|
||||
optional: true,
|
||||
properties: PLAYBACK_TRACK_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { SpotifyGetPlaylistParams, SpotifyGetPlaylistResponse } from '@/tools/spotify/types'
|
||||
import { PLAYLIST_OWNER_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetPlaylistTool: ToolConfig<
|
||||
SpotifyGetPlaylistParams,
|
||||
SpotifyGetPlaylistResponse
|
||||
> = {
|
||||
id: 'spotify_get_playlist',
|
||||
name: 'Spotify Get Playlist',
|
||||
description: 'Get detailed information about a playlist on Spotify by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the playlist',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code for track availability (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/playlists/${params.playlistId}`
|
||||
if (params.market) {
|
||||
url += `?market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetPlaylistResponse> => {
|
||||
const playlist = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
public: playlist.public,
|
||||
collaborative: playlist.collaborative,
|
||||
owner: {
|
||||
id: playlist.owner?.id || '',
|
||||
display_name: playlist.owner?.display_name || '',
|
||||
},
|
||||
image_url: playlist.images?.[0]?.url || null,
|
||||
total_tracks: playlist.tracks?.total || 0,
|
||||
snapshot_id: playlist.snapshot_id,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify playlist ID' },
|
||||
name: { type: 'string', description: 'Playlist name' },
|
||||
description: { type: 'string', description: 'Playlist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether the playlist is public' },
|
||||
collaborative: { type: 'boolean', description: 'Whether the playlist is collaborative' },
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Playlist owner information',
|
||||
properties: PLAYLIST_OWNER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
image_url: { type: 'string', description: 'Playlist cover image URL', optional: true },
|
||||
total_tracks: { type: 'number', description: 'Total number of tracks' },
|
||||
snapshot_id: { type: 'string', description: 'Playlist snapshot ID for versioning' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetPlaylistCoverParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
}
|
||||
|
||||
interface SpotifyGetPlaylistCoverResponse extends ToolResponse {
|
||||
output: {
|
||||
images: Array<{
|
||||
url: string
|
||||
width: number | null
|
||||
height: number | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetPlaylistCoverTool: ToolConfig<
|
||||
SpotifyGetPlaylistCoverParams,
|
||||
SpotifyGetPlaylistCoverResponse
|
||||
> = {
|
||||
id: 'spotify_get_playlist_cover',
|
||||
name: 'Spotify Get Playlist Cover',
|
||||
description: "Get a playlist's cover image.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/images`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetPlaylistCoverResponse> => {
|
||||
const images = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
images: (images || []).map((img: any) => ({
|
||||
url: img.url,
|
||||
width: img.width || null,
|
||||
height: img.height || null,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
images: { type: 'json', description: 'List of cover images' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type {
|
||||
SpotifyGetPlaylistTracksParams,
|
||||
SpotifyGetPlaylistTracksResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import { TRACK_LIST_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetPlaylistTracksTool: ToolConfig<
|
||||
SpotifyGetPlaylistTracksParams,
|
||||
SpotifyGetPlaylistTracksResponse
|
||||
> = {
|
||||
id: 'spotify_get_playlist_tracks',
|
||||
name: 'Spotify Get Playlist Tracks',
|
||||
description: 'Get the tracks in a Spotify playlist.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the playlist',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 50,
|
||||
description: 'Maximum number of tracks to return (1-100)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of the first track to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code for track availability (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 50, 1), 100)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks?limit=${limit}&offset=${offset}`
|
||||
if (params.market) {
|
||||
url += `&market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetPlaylistTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || [])
|
||||
.filter((item: any) => item.track !== null)
|
||||
.map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
added_by: item.added_by?.id || '',
|
||||
track: {
|
||||
id: item.track.id,
|
||||
name: item.track.name,
|
||||
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: item.track.album?.id || '',
|
||||
name: item.track.album?.name || '',
|
||||
image_url: item.track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: item.track.duration_ms,
|
||||
popularity: item.track.popularity,
|
||||
external_url: item.track.external_urls?.spotify || '',
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || tracks.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of tracks in the playlist',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
added_at: { type: 'string', description: 'When the track was added' },
|
||||
added_by: { type: 'string', description: 'User ID who added the track' },
|
||||
track: {
|
||||
type: 'object',
|
||||
description: 'Track information',
|
||||
properties: TRACK_LIST_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of tracks in playlist' },
|
||||
next: { type: 'string', description: 'URL for next page of results', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { PLAYBACK_TRACK_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetQueueParams {
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
interface SpotifyGetQueueResponse extends ToolResponse {
|
||||
output: {
|
||||
currently_playing: {
|
||||
id: string
|
||||
name: string
|
||||
artists: Array<{ id: string; name: string }>
|
||||
album: {
|
||||
id: string
|
||||
name: string
|
||||
image_url: string | null
|
||||
}
|
||||
duration_ms: number
|
||||
} | null
|
||||
queue: Array<{
|
||||
id: string
|
||||
name: string
|
||||
artists: Array<{ id: string; name: string }>
|
||||
album: {
|
||||
id: string
|
||||
name: string
|
||||
image_url: string | null
|
||||
}
|
||||
duration_ms: number
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetQueueTool: ToolConfig<SpotifyGetQueueParams, SpotifyGetQueueResponse> = {
|
||||
id: 'spotify_get_queue',
|
||||
name: 'Spotify Get Queue',
|
||||
description: "Get the user's playback queue.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-state'],
|
||||
},
|
||||
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/player/queue',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetQueueResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const formatTrack = (track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
currently_playing: data.currently_playing ? formatTrack(data.currently_playing) : null,
|
||||
queue: (data.queue || []).map(formatTrack),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
currently_playing: {
|
||||
type: 'object',
|
||||
description: 'Currently playing track',
|
||||
optional: true,
|
||||
properties: PLAYBACK_TRACK_OUTPUT_PROPERTIES,
|
||||
},
|
||||
queue: {
|
||||
type: 'array',
|
||||
description: 'Upcoming tracks in queue',
|
||||
items: { type: 'object', properties: PLAYBACK_TRACK_OUTPUT_PROPERTIES },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type {
|
||||
SpotifyGetRecentlyPlayedParams,
|
||||
SpotifyGetRecentlyPlayedResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import { CURRENTLY_PLAYING_TRACK_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetRecentlyPlayedTool: ToolConfig<
|
||||
SpotifyGetRecentlyPlayedParams,
|
||||
SpotifyGetRecentlyPlayedResponse
|
||||
> = {
|
||||
id: 'spotify_get_recently_played',
|
||||
name: 'Spotify Get Recently Played',
|
||||
description: "Get the user's recently played tracks.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-recently-played'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of tracks to return (1-50)',
|
||||
},
|
||||
after: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unix timestamp in milliseconds. Returns items after this cursor.',
|
||||
},
|
||||
before: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unix timestamp in milliseconds. Returns items before this cursor.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
let url = `https://api.spotify.com/v1/me/player/recently-played?limit=${limit}`
|
||||
if (params.after) {
|
||||
url += `&after=${params.after}`
|
||||
}
|
||||
if (params.before) {
|
||||
url += `&before=${params.before}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetRecentlyPlayedResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const items = (data.items || []).map((item: any) => ({
|
||||
played_at: item.played_at,
|
||||
track: {
|
||||
id: item.track.id,
|
||||
name: item.track.name,
|
||||
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: item.track.album?.id || '',
|
||||
name: item.track.album?.name || '',
|
||||
image_url: item.track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: item.track.duration_ms,
|
||||
external_url: item.track.external_urls?.spotify || '',
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Recently played tracks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
played_at: { type: 'string', description: 'When the track was played' },
|
||||
track: {
|
||||
type: 'object',
|
||||
description: 'Track information',
|
||||
properties: CURRENTLY_PLAYING_TRACK_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetSavedAlbumsParams {
|
||||
accessToken: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetSavedAlbumsResponse extends ToolResponse {
|
||||
output: {
|
||||
albums: Array<{
|
||||
added_at: string
|
||||
album: {
|
||||
id: string
|
||||
name: string
|
||||
artists: Array<{ id: string; name: string }>
|
||||
total_tracks: number
|
||||
release_date: string
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetSavedAlbumsTool: ToolConfig<
|
||||
SpotifyGetSavedAlbumsParams,
|
||||
SpotifyGetSavedAlbumsResponse
|
||||
> = {
|
||||
id: 'spotify_get_saved_albums',
|
||||
name: 'Spotify Get Saved Albums',
|
||||
description: "Get the user's saved albums.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of albums to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first album to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/me/albums?limit=${limit}&offset=${offset}`
|
||||
if (params.market) url += `&market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetSavedAlbumsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
albums: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
album: {
|
||||
id: item.album.id,
|
||||
name: item.album.name,
|
||||
artists: item.album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
total_tracks: item.album.total_tracks,
|
||||
release_date: item.album.release_date,
|
||||
image_url: item.album.images?.[0]?.url || null,
|
||||
external_url: item.album.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: {
|
||||
type: 'array',
|
||||
description: 'List of saved albums',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
added_at: { type: 'string', description: 'When the album was saved' },
|
||||
album: {
|
||||
type: 'object',
|
||||
description: 'Album information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify album ID' },
|
||||
name: { type: 'string', description: 'Album name' },
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of artists',
|
||||
items: { type: 'object', properties: SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
total_tracks: { type: 'number', description: 'Total number of tracks' },
|
||||
release_date: { type: 'string', description: 'Release date' },
|
||||
image_url: { type: 'string', description: 'Album cover image URL', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total saved albums' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetSavedAudiobooksParams {
|
||||
accessToken: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
interface SpotifyGetSavedAudiobooksResponse extends ToolResponse {
|
||||
output: {
|
||||
audiobooks: Array<{
|
||||
added_at: string
|
||||
audiobook: {
|
||||
id: string
|
||||
name: string
|
||||
authors: Array<{ name: string }>
|
||||
total_chapters: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetSavedAudiobooksTool: ToolConfig<
|
||||
SpotifyGetSavedAudiobooksParams,
|
||||
SpotifyGetSavedAudiobooksResponse
|
||||
> = {
|
||||
id: 'spotify_get_saved_audiobooks',
|
||||
name: 'Spotify Get Saved Audiobooks',
|
||||
description: "Get the user's saved audiobooks.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of audiobooks to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first audiobook to return for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
return `https://api.spotify.com/v1/me/audiobooks?limit=${limit}&offset=${offset}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetSavedAudiobooksResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
audiobooks: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
audiobook: {
|
||||
id: item.audiobook?.id || item.id,
|
||||
name: item.audiobook?.name || item.name,
|
||||
authors: item.audiobook?.authors || item.authors || [],
|
||||
total_chapters: item.audiobook?.total_chapters || item.total_chapters || 0,
|
||||
image_url: item.audiobook?.images?.[0]?.url || item.images?.[0]?.url || null,
|
||||
external_url:
|
||||
item.audiobook?.external_urls?.spotify || item.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
audiobooks: { type: 'json', description: 'List of saved audiobooks' },
|
||||
total: { type: 'number', description: 'Total saved audiobooks' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetSavedEpisodesParams {
|
||||
accessToken: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetSavedEpisodesResponse extends ToolResponse {
|
||||
output: {
|
||||
episodes: Array<{
|
||||
added_at: string
|
||||
episode: {
|
||||
id: string
|
||||
name: string
|
||||
duration_ms: number
|
||||
release_date: string
|
||||
show: { id: string; name: string }
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetSavedEpisodesTool: ToolConfig<
|
||||
SpotifyGetSavedEpisodesParams,
|
||||
SpotifyGetSavedEpisodesResponse
|
||||
> = {
|
||||
id: 'spotify_get_saved_episodes',
|
||||
name: 'Spotify Get Saved Episodes',
|
||||
description: "Get the user's saved podcast episodes.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read', 'user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of episodes to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first episode to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/me/episodes?limit=${limit}&offset=${offset}`
|
||||
if (params.market) url += `&market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetSavedEpisodesResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
episodes: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
episode: {
|
||||
id: item.episode.id,
|
||||
name: item.episode.name,
|
||||
duration_ms: item.episode.duration_ms || 0,
|
||||
release_date: item.episode.release_date || '',
|
||||
show: { id: item.episode.show?.id || '', name: item.episode.show?.name || '' },
|
||||
image_url: item.episode.images?.[0]?.url || null,
|
||||
external_url: item.episode.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
episodes: { type: 'json', description: 'List of saved episodes' },
|
||||
total: { type: 'number', description: 'Total saved episodes' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetSavedShowsParams {
|
||||
accessToken: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
interface SpotifyGetSavedShowsResponse extends ToolResponse {
|
||||
output: {
|
||||
shows: Array<{
|
||||
added_at: string
|
||||
show: {
|
||||
id: string
|
||||
name: string
|
||||
publisher: string
|
||||
total_episodes: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetSavedShowsTool: ToolConfig<
|
||||
SpotifyGetSavedShowsParams,
|
||||
SpotifyGetSavedShowsResponse
|
||||
> = {
|
||||
id: 'spotify_get_saved_shows',
|
||||
name: 'Spotify Get Saved Shows',
|
||||
description: "Get the user's saved podcast shows.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of shows to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first show to return for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
return `https://api.spotify.com/v1/me/shows?limit=${limit}&offset=${offset}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetSavedShowsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
shows: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
show: {
|
||||
id: item.show.id,
|
||||
name: item.show.name,
|
||||
publisher: item.show.publisher || '',
|
||||
total_episodes: item.show.total_episodes || 0,
|
||||
image_url: item.show.images?.[0]?.url || null,
|
||||
external_url: item.show.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
shows: { type: 'json', description: 'List of saved shows' },
|
||||
total: { type: 'number', description: 'Total saved shows' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type {
|
||||
SpotifyGetSavedTracksParams,
|
||||
SpotifyGetSavedTracksResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import { TRACK_LIST_OUTPUT_PROPERTIES } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetSavedTracksTool: ToolConfig<
|
||||
SpotifyGetSavedTracksParams,
|
||||
SpotifyGetSavedTracksResponse
|
||||
> = {
|
||||
id: 'spotify_get_saved_tracks',
|
||||
name: 'Spotify Get Saved Tracks',
|
||||
description: "Get the current user's saved/liked tracks from their library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of tracks to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of the first track to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/me/tracks?limit=${limit}&offset=${offset}`
|
||||
if (params.market) {
|
||||
url += `&market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetSavedTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
track: {
|
||||
id: item.track.id,
|
||||
name: item.track.name,
|
||||
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: item.track.album?.id || '',
|
||||
name: item.track.album?.name || '',
|
||||
image_url: item.track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: item.track.duration_ms,
|
||||
popularity: item.track.popularity,
|
||||
external_url: item.track.external_urls?.spotify || '',
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || tracks.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: "User's saved tracks",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
added_at: { type: 'string', description: 'When the track was saved' },
|
||||
track: {
|
||||
type: 'object',
|
||||
description: 'Track information',
|
||||
properties: TRACK_LIST_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of saved tracks' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetShowParams {
|
||||
accessToken: string
|
||||
showId: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetShowResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
publisher: string
|
||||
total_episodes: number
|
||||
explicit: boolean
|
||||
languages: string[]
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetShowTool: ToolConfig<SpotifyGetShowParams, SpotifyGetShowResponse> = {
|
||||
id: 'spotify_get_show',
|
||||
name: 'Spotify Get Show',
|
||||
description: 'Get details for a podcast show.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify show ID',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/shows/${params.showId}`
|
||||
if (params.market) url += `?market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetShowResponse> => {
|
||||
const show = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: show.id,
|
||||
name: show.name,
|
||||
description: show.description || '',
|
||||
publisher: show.publisher || '',
|
||||
total_episodes: show.total_episodes || 0,
|
||||
explicit: show.explicit || false,
|
||||
languages: show.languages || [],
|
||||
image_url: show.images?.[0]?.url || null,
|
||||
external_url: show.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Show ID' },
|
||||
name: { type: 'string', description: 'Show name' },
|
||||
description: { type: 'string', description: 'Show description' },
|
||||
publisher: { type: 'string', description: 'Publisher name' },
|
||||
total_episodes: { type: 'number', description: 'Total episodes' },
|
||||
explicit: { type: 'boolean', description: 'Contains explicit content' },
|
||||
languages: { type: 'json', description: 'Languages' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetShowEpisodesParams {
|
||||
accessToken: string
|
||||
showId: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetShowEpisodesResponse extends ToolResponse {
|
||||
output: {
|
||||
episodes: Array<{
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
duration_ms: number
|
||||
release_date: string
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
total: number
|
||||
next: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetShowEpisodesTool: ToolConfig<
|
||||
SpotifyGetShowEpisodesParams,
|
||||
SpotifyGetShowEpisodesResponse
|
||||
> = {
|
||||
id: 'spotify_get_show_episodes',
|
||||
name: 'Spotify Get Show Episodes',
|
||||
description: 'Get episodes from a podcast show.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify show ID',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of episodes to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of first episode to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/shows/${params.showId}/episodes?limit=${limit}&offset=${offset}`
|
||||
if (params.market) url += `&market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetShowEpisodesResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
episodes: (data.items || []).map((ep: any) => ({
|
||||
id: ep.id,
|
||||
name: ep.name,
|
||||
description: ep.description || '',
|
||||
duration_ms: ep.duration_ms || 0,
|
||||
release_date: ep.release_date || '',
|
||||
image_url: ep.images?.[0]?.url || null,
|
||||
external_url: ep.external_urls?.spotify || '',
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
episodes: { type: 'json', description: 'List of episodes' },
|
||||
total: { type: 'number', description: 'Total episodes' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetShowsParams {
|
||||
accessToken: string
|
||||
showIds: string
|
||||
market?: string
|
||||
}
|
||||
|
||||
interface SpotifyGetShowsResponse extends ToolResponse {
|
||||
output: {
|
||||
shows: Array<{
|
||||
id: string
|
||||
name: string
|
||||
publisher: string
|
||||
total_episodes: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetShowsTool: ToolConfig<SpotifyGetShowsParams, SpotifyGetShowsResponse> = {
|
||||
id: 'spotify_get_shows',
|
||||
name: 'Spotify Get Multiple Shows',
|
||||
description: 'Get details for multiple podcast shows.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated show IDs (max 50)',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.showIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
let url = `https://api.spotify.com/v1/shows?ids=${ids}`
|
||||
if (params.market) url += `&market=${params.market}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetShowsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
shows: (data.shows || []).map((show: any) => ({
|
||||
id: show.id,
|
||||
name: show.name,
|
||||
publisher: show.publisher || '',
|
||||
total_episodes: show.total_episodes || 0,
|
||||
image_url: show.images?.[0]?.url || null,
|
||||
external_url: show.external_urls?.spotify || '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
shows: { type: 'json', description: 'List of shows' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { SpotifyGetTopArtistsResponse, SpotifyGetTopItemsParams } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetTopArtistsTool: ToolConfig<
|
||||
SpotifyGetTopItemsParams,
|
||||
SpotifyGetTopArtistsResponse
|
||||
> = {
|
||||
id: 'spotify_get_top_artists',
|
||||
name: 'Spotify Get Top Artists',
|
||||
description: "Get the current user's top artists based on listening history.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-top-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
time_range: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 'medium_term',
|
||||
description: 'Time range: short_term (~4 weeks), medium_term (~6 months), long_term (years)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of artists to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of the first artist to return for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const timeRange = params.time_range || 'medium_term'
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
return `https://api.spotify.com/v1/me/top/artists?time_range=${timeRange}&limit=${limit}&offset=${offset}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetTopArtistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const artists = (data.items || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
artists,
|
||||
total: data.total || artists.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: "User's top artists",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify artist ID' },
|
||||
name: { type: 'string', description: 'Artist name' },
|
||||
genres: { type: 'array', description: 'List of genres' },
|
||||
popularity: { type: 'number', description: 'Popularity score' },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Artist image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of top artists' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { SpotifyGetTopItemsParams, SpotifyGetTopTracksResponse } from '@/tools/spotify/types'
|
||||
import {
|
||||
SIMPLIFIED_ALBUM_OUTPUT_PROPERTIES,
|
||||
SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetTopTracksTool: ToolConfig<
|
||||
SpotifyGetTopItemsParams,
|
||||
SpotifyGetTopTracksResponse
|
||||
> = {
|
||||
id: 'spotify_get_top_tracks',
|
||||
name: 'Spotify Get Top Tracks',
|
||||
description: "Get the current user's top tracks based on listening history.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-top-read'],
|
||||
},
|
||||
|
||||
params: {
|
||||
time_range: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 'medium_term',
|
||||
description: 'Time range: short_term (~4 weeks), medium_term (~6 months), long_term (years)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Number of tracks to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of the first track to return for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const timeRange = params.time_range || 'medium_term'
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
return `https://api.spotify.com/v1/me/top/tracks?time_range=${timeRange}&limit=${limit}&offset=${offset}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetTopTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
popularity: track.popularity,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || tracks.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: "User's top tracks",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify track ID' },
|
||||
name: { type: 'string', description: 'Track name' },
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of artists',
|
||||
items: { type: 'object', properties: SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
album: {
|
||||
type: 'object',
|
||||
description: 'Album information',
|
||||
properties: SIMPLIFIED_ALBUM_OUTPUT_PROPERTIES,
|
||||
},
|
||||
duration_ms: { type: 'number', description: 'Duration in milliseconds' },
|
||||
popularity: { type: 'number', description: 'Popularity score' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of top tracks' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { SpotifyGetTrackParams, SpotifyGetTrackResponse } from '@/tools/spotify/types'
|
||||
import {
|
||||
SIMPLIFIED_ALBUM_OUTPUT_PROPERTIES,
|
||||
SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetTrackTool: ToolConfig<SpotifyGetTrackParams, SpotifyGetTrackResponse> = {
|
||||
id: 'spotify_get_track',
|
||||
name: 'Spotify Get Track',
|
||||
description: 'Get detailed information about a specific track on Spotify by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
trackId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the track',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code for track availability (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/tracks/${params.trackId}`
|
||||
if (params.market) {
|
||||
url += `?market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetTrackResponse> => {
|
||||
const track = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
explicit: track.explicit,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
uri: track.uri,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify track ID' },
|
||||
name: { type: 'string', description: 'Track name' },
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of artists',
|
||||
items: { type: 'object', properties: SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
album: {
|
||||
type: 'object',
|
||||
description: 'Album information',
|
||||
properties: SIMPLIFIED_ALBUM_OUTPUT_PROPERTIES,
|
||||
},
|
||||
duration_ms: { type: 'number', description: 'Track duration in milliseconds' },
|
||||
explicit: { type: 'boolean', description: 'Whether the track has explicit content' },
|
||||
popularity: { type: 'number', description: 'Popularity score (0-100)' },
|
||||
preview_url: { type: 'string', description: 'URL to 30-second preview', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
uri: { type: 'string', description: 'Spotify URI for the track' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { SpotifyGetTracksParams, SpotifyGetTracksResponse } from '@/tools/spotify/types'
|
||||
import {
|
||||
SIMPLIFIED_ALBUM_OUTPUT_PROPERTIES,
|
||||
SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetTracksTool: ToolConfig<SpotifyGetTracksParams, SpotifyGetTracksResponse> = {
|
||||
id: 'spotify_get_tracks',
|
||||
name: 'Spotify Get Multiple Tracks',
|
||||
description: 'Get detailed information about multiple tracks on Spotify by their IDs (up to 50).',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
trackIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of Spotify track IDs (max 50)',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code for track availability (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/tracks?ids=${encodeURIComponent(params.trackIds)}`
|
||||
if (params.market) {
|
||||
url += `&market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.tracks || [])
|
||||
.filter((t: any) => t !== null)
|
||||
.map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
explicit: track.explicit,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of tracks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify track ID' },
|
||||
name: { type: 'string', description: 'Track name' },
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of artists',
|
||||
items: { type: 'object', properties: SIMPLIFIED_ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
album: {
|
||||
type: 'object',
|
||||
description: 'Album information',
|
||||
properties: SIMPLIFIED_ALBUM_OUTPUT_PROPERTIES,
|
||||
},
|
||||
duration_ms: { type: 'number', description: 'Track duration in milliseconds' },
|
||||
explicit: { type: 'boolean', description: 'Whether the track has explicit content' },
|
||||
popularity: { type: 'number', description: 'Popularity score (0-100)' },
|
||||
preview_url: { type: 'string', description: 'URL to 30-second preview', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
SpotifyGetUserPlaylistsParams,
|
||||
SpotifyGetUserPlaylistsResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyGetUserPlaylistsTool: ToolConfig<
|
||||
SpotifyGetUserPlaylistsParams,
|
||||
SpotifyGetUserPlaylistsResponse
|
||||
> = {
|
||||
id: 'spotify_get_user_playlists',
|
||||
name: 'Spotify Get User Playlists',
|
||||
description: "Get the current user's playlists on Spotify.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-read-private', 'playlist-read-collaborative'],
|
||||
},
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Maximum number of playlists to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of the first playlist to return for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
return `https://api.spotify.com/v1/me/playlists?limit=${limit}&offset=${offset}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetUserPlaylistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const playlists = (data.items || []).map((playlist: any) => ({
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
public: playlist.public,
|
||||
collaborative: playlist.collaborative,
|
||||
owner: playlist.owner?.display_name || '',
|
||||
total_tracks: playlist.tracks?.total || 0,
|
||||
image_url: playlist.images?.[0]?.url || null,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
playlists,
|
||||
total: data.total || playlists.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
playlists: {
|
||||
type: 'array',
|
||||
description: "User's playlists",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify playlist ID' },
|
||||
name: { type: 'string', description: 'Playlist name' },
|
||||
description: { type: 'string', description: 'Playlist description' },
|
||||
public: { type: 'boolean', description: 'Whether public' },
|
||||
collaborative: { type: 'boolean', description: 'Whether collaborative' },
|
||||
owner: { type: 'string', description: 'Owner display name' },
|
||||
total_tracks: { type: 'number', description: 'Number of tracks' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of playlists' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetUserProfileParams {
|
||||
accessToken: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
interface SpotifyGetUserProfileResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
display_name: string | null
|
||||
followers: number
|
||||
image_url: string | null
|
||||
external_url: string
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetUserProfileTool: ToolConfig<
|
||||
SpotifyGetUserProfileParams,
|
||||
SpotifyGetUserProfileResponse
|
||||
> = {
|
||||
id: 'spotify_get_user_profile',
|
||||
name: 'Spotify Get User Profile',
|
||||
description: "Get a user's public profile.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify user ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/users/${params.userId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetUserProfileResponse> => {
|
||||
const user = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: user.id,
|
||||
display_name: user.display_name || null,
|
||||
followers: user.followers?.total || 0,
|
||||
image_url: user.images?.[0]?.url || null,
|
||||
external_url: user.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'User ID' },
|
||||
display_name: { type: 'string', description: 'Display name' },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Profile image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Search & Discovery
|
||||
|
||||
export { spotifyAddPlaylistCoverTool } from './add_playlist_cover'
|
||||
// Player Controls
|
||||
export { spotifyAddToQueueTool } from './add_to_queue'
|
||||
export { spotifyAddTracksToPlaylistTool } from './add_tracks_to_playlist'
|
||||
export { spotifyCheckFollowingTool } from './check_following'
|
||||
export { spotifyCheckPlaylistFollowersTool } from './check_playlist_followers'
|
||||
export { spotifyCheckSavedAlbumsTool } from './check_saved_albums'
|
||||
export { spotifyCheckSavedAudiobooksTool } from './check_saved_audiobooks'
|
||||
export { spotifyCheckSavedEpisodesTool } from './check_saved_episodes'
|
||||
export { spotifyCheckSavedShowsTool } from './check_saved_shows'
|
||||
export { spotifyCheckSavedTracksTool } from './check_saved_tracks'
|
||||
export { spotifyCreatePlaylistTool } from './create_playlist'
|
||||
export { spotifyFollowArtistsTool } from './follow_artists'
|
||||
export { spotifyFollowPlaylistTool } from './follow_playlist'
|
||||
// Albums
|
||||
export { spotifyGetAlbumTool } from './get_album'
|
||||
export { spotifyGetAlbumTracksTool } from './get_album_tracks'
|
||||
export { spotifyGetAlbumsTool } from './get_albums'
|
||||
// Artists
|
||||
export { spotifyGetArtistTool } from './get_artist'
|
||||
export { spotifyGetArtistAlbumsTool } from './get_artist_albums'
|
||||
export { spotifyGetArtistTopTracksTool } from './get_artist_top_tracks'
|
||||
export { spotifyGetArtistsTool } from './get_artists'
|
||||
// Audiobooks
|
||||
export { spotifyGetAudiobookTool } from './get_audiobook'
|
||||
export { spotifyGetAudiobookChaptersTool } from './get_audiobook_chapters'
|
||||
export { spotifyGetAudiobooksTool } from './get_audiobooks'
|
||||
export { spotifyGetCategoriesTool } from './get_categories'
|
||||
// User Profile & Library
|
||||
export { spotifyGetCurrentUserTool } from './get_current_user'
|
||||
export { spotifyGetCurrentlyPlayingTool } from './get_currently_playing'
|
||||
export { spotifyGetDevicesTool } from './get_devices'
|
||||
// Episodes
|
||||
export { spotifyGetEpisodeTool } from './get_episode'
|
||||
export { spotifyGetEpisodesTool } from './get_episodes'
|
||||
export { spotifyGetFollowedArtistsTool } from './get_followed_artists'
|
||||
export { spotifyGetMarketsTool } from './get_markets'
|
||||
// Browse
|
||||
export { spotifyGetNewReleasesTool } from './get_new_releases'
|
||||
// Player Controls
|
||||
export { spotifyGetPlaybackStateTool } from './get_playback_state'
|
||||
// Playlists
|
||||
export { spotifyGetPlaylistTool } from './get_playlist'
|
||||
export { spotifyGetPlaylistCoverTool } from './get_playlist_cover'
|
||||
export { spotifyGetPlaylistTracksTool } from './get_playlist_tracks'
|
||||
export { spotifyGetQueueTool } from './get_queue'
|
||||
export { spotifyGetRecentlyPlayedTool } from './get_recently_played'
|
||||
export { spotifyGetSavedAlbumsTool } from './get_saved_albums'
|
||||
export { spotifyGetSavedAudiobooksTool } from './get_saved_audiobooks'
|
||||
export { spotifyGetSavedEpisodesTool } from './get_saved_episodes'
|
||||
export { spotifyGetSavedShowsTool } from './get_saved_shows'
|
||||
export { spotifyGetSavedTracksTool } from './get_saved_tracks'
|
||||
// Shows (Podcasts)
|
||||
export { spotifyGetShowTool } from './get_show'
|
||||
export { spotifyGetShowEpisodesTool } from './get_show_episodes'
|
||||
export { spotifyGetShowsTool } from './get_shows'
|
||||
export { spotifyGetTopArtistsTool } from './get_top_artists'
|
||||
export { spotifyGetTopTracksTool } from './get_top_tracks'
|
||||
// Tracks
|
||||
export { spotifyGetTrackTool } from './get_track'
|
||||
export { spotifyGetTracksTool } from './get_tracks'
|
||||
export { spotifyGetUserPlaylistsTool } from './get_user_playlists'
|
||||
export { spotifyGetUserProfileTool } from './get_user_profile'
|
||||
export { spotifyPauseTool } from './pause'
|
||||
export { spotifyPlayTool } from './play'
|
||||
// Library Management
|
||||
export { spotifyRemoveSavedAlbumsTool } from './remove_saved_albums'
|
||||
export { spotifyRemoveSavedAudiobooksTool } from './remove_saved_audiobooks'
|
||||
export { spotifyRemoveSavedEpisodesTool } from './remove_saved_episodes'
|
||||
export { spotifyRemoveSavedShowsTool } from './remove_saved_shows'
|
||||
export { spotifyRemoveSavedTracksTool } from './remove_saved_tracks'
|
||||
export { spotifyRemoveTracksFromPlaylistTool } from './remove_tracks_from_playlist'
|
||||
export { spotifyReorderPlaylistItemsTool } from './reorder_playlist_items'
|
||||
export { spotifyReplacePlaylistItemsTool } from './replace_playlist_items'
|
||||
export { spotifySaveAlbumsTool } from './save_albums'
|
||||
export { spotifySaveAudiobooksTool } from './save_audiobooks'
|
||||
export { spotifySaveEpisodesTool } from './save_episodes'
|
||||
export { spotifySaveShowsTool } from './save_shows'
|
||||
export { spotifySaveTracksTool } from './save_tracks'
|
||||
export { spotifySearchTool } from './search'
|
||||
export { spotifySeekTool } from './seek'
|
||||
export { spotifySetRepeatTool } from './set_repeat'
|
||||
export { spotifySetShuffleTool } from './set_shuffle'
|
||||
export { spotifySetVolumeTool } from './set_volume'
|
||||
export { spotifySkipNextTool } from './skip_next'
|
||||
export { spotifySkipPreviousTool } from './skip_previous'
|
||||
export { spotifyTransferPlaybackTool } from './transfer_playback'
|
||||
export { spotifyUnfollowArtistsTool } from './unfollow_artists'
|
||||
export { spotifyUnfollowPlaylistTool } from './unfollow_playlist'
|
||||
export { spotifyUpdatePlaylistTool } from './update_playlist'
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { SpotifyPauseParams, SpotifyPauseResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyPauseTool: ToolConfig<SpotifyPauseParams, SpotifyPauseResponse> = {
|
||||
id: 'spotify_pause',
|
||||
name: 'Spotify Pause',
|
||||
description: 'Pause playback on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID to pause. If not provided, pauses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player/pause'
|
||||
if (params.device_id) {
|
||||
url += `?device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyPauseResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether playback was paused' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { SpotifyPlayParams, SpotifyPlayResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyPlayTool: ToolConfig<SpotifyPlayParams, SpotifyPlayResponse> = {
|
||||
id: 'spotify_play',
|
||||
name: 'Spotify Play',
|
||||
description:
|
||||
'Start or resume playback on Spotify. Can play specific tracks, albums, or playlists.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID to play on. If not provided, plays on active device.',
|
||||
},
|
||||
context_uri: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Spotify URI of album, artist, or playlist to play (e.g., "spotify:album:xxx")',
|
||||
},
|
||||
uris: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated track URIs to play (e.g., "spotify:track:xxx,spotify:track:yyy")',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position in context to start playing (0-based index)',
|
||||
},
|
||||
position_ms: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position in track to start from (in milliseconds)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player/play'
|
||||
if (params.device_id) {
|
||||
url += `?device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: any = {}
|
||||
if (params.context_uri) {
|
||||
body.context_uri = params.context_uri
|
||||
}
|
||||
if (params.uris) {
|
||||
body.uris = params.uris.split(',').map((uri) => uri.trim())
|
||||
}
|
||||
if (params.offset !== undefined) {
|
||||
body.offset = { position: params.offset }
|
||||
}
|
||||
if (params.position_ms !== undefined) {
|
||||
body.position_ms = params.position_ms
|
||||
}
|
||||
return Object.keys(body).length > 0 ? body : undefined
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyPlayResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether playback started successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyRemoveSavedAlbumsParams {
|
||||
accessToken: string
|
||||
albumIds: string
|
||||
}
|
||||
|
||||
interface SpotifyRemoveSavedAlbumsResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyRemoveSavedAlbumsTool: ToolConfig<
|
||||
SpotifyRemoveSavedAlbumsParams,
|
||||
SpotifyRemoveSavedAlbumsResponse
|
||||
> = {
|
||||
id: 'spotify_remove_saved_albums',
|
||||
name: 'Spotify Remove Saved Albums',
|
||||
description: "Remove albums from the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
albumIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated album IDs (max 20)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/albums',
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
ids: params.albumIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 20),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyRemoveSavedAlbumsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether albums were removed' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyRemoveSavedAudiobooksParams {
|
||||
accessToken: string
|
||||
audiobookIds: string
|
||||
}
|
||||
|
||||
interface SpotifyRemoveSavedAudiobooksResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyRemoveSavedAudiobooksTool: ToolConfig<
|
||||
SpotifyRemoveSavedAudiobooksParams,
|
||||
SpotifyRemoveSavedAudiobooksResponse
|
||||
> = {
|
||||
id: 'spotify_remove_saved_audiobooks',
|
||||
name: 'Spotify Remove Saved Audiobooks',
|
||||
description: "Remove audiobooks from the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
audiobookIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated audiobook IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.audiobookIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/audiobooks?ids=${ids}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyRemoveSavedAudiobooksResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether audiobooks were removed' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyRemoveSavedEpisodesParams {
|
||||
accessToken: string
|
||||
episodeIds: string
|
||||
}
|
||||
|
||||
interface SpotifyRemoveSavedEpisodesResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyRemoveSavedEpisodesTool: ToolConfig<
|
||||
SpotifyRemoveSavedEpisodesParams,
|
||||
SpotifyRemoveSavedEpisodesResponse
|
||||
> = {
|
||||
id: 'spotify_remove_saved_episodes',
|
||||
name: 'Spotify Remove Saved Episodes',
|
||||
description: "Remove podcast episodes from the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
episodeIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated episode IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.episodeIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/episodes?ids=${ids}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyRemoveSavedEpisodesResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether episodes were removed' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyRemoveSavedShowsParams {
|
||||
accessToken: string
|
||||
showIds: string
|
||||
}
|
||||
|
||||
interface SpotifyRemoveSavedShowsResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyRemoveSavedShowsTool: ToolConfig<
|
||||
SpotifyRemoveSavedShowsParams,
|
||||
SpotifyRemoveSavedShowsResponse
|
||||
> = {
|
||||
id: 'spotify_remove_saved_shows',
|
||||
name: 'Spotify Remove Saved Shows',
|
||||
description: "Remove podcast shows from the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated show IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.showIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/shows?ids=${ids}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyRemoveSavedShowsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether shows were removed' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyRemoveSavedTracksParams {
|
||||
accessToken: string
|
||||
trackIds: string
|
||||
}
|
||||
|
||||
interface SpotifyRemoveSavedTracksResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyRemoveSavedTracksTool: ToolConfig<
|
||||
SpotifyRemoveSavedTracksParams,
|
||||
SpotifyRemoveSavedTracksResponse
|
||||
> = {
|
||||
id: 'spotify_remove_saved_tracks',
|
||||
name: 'Spotify Remove Saved Tracks',
|
||||
description: "Remove tracks from the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
trackIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated track IDs to remove (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/tracks',
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
ids: params.trackIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyRemoveSavedTracksResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether tracks were removed successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
SpotifyRemoveTracksFromPlaylistParams,
|
||||
SpotifyRemoveTracksFromPlaylistResponse,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifyRemoveTracksFromPlaylistTool: ToolConfig<
|
||||
SpotifyRemoveTracksFromPlaylistParams,
|
||||
SpotifyRemoveTracksFromPlaylistResponse
|
||||
> = {
|
||||
id: 'spotify_remove_tracks_from_playlist',
|
||||
name: 'Spotify Remove Tracks from Playlist',
|
||||
description: 'Remove one or more tracks from a Spotify playlist.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the playlist',
|
||||
},
|
||||
uris: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated Spotify URIs to remove (e.g., "spotify:track:xxx,spotify:track:yyy")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const uris = params.uris.split(',').map((uri) => ({ uri: uri.trim() }))
|
||||
return { tracks: uris }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyRemoveTracksFromPlaylistResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
snapshot_id: data.snapshot_id,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID after modification' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyReorderPlaylistItemsParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
range_start: number
|
||||
insert_before: number
|
||||
range_length?: number
|
||||
snapshot_id?: string
|
||||
}
|
||||
|
||||
interface SpotifyReorderPlaylistItemsResponse extends ToolResponse {
|
||||
output: {
|
||||
snapshot_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyReorderPlaylistItemsTool: ToolConfig<
|
||||
SpotifyReorderPlaylistItemsParams,
|
||||
SpotifyReorderPlaylistItemsResponse
|
||||
> = {
|
||||
id: 'spotify_reorder_playlist_items',
|
||||
name: 'Spotify Reorder Playlist Items',
|
||||
description: 'Move tracks to a different position in a playlist.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
range_start: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Start index of items to reorder',
|
||||
},
|
||||
insert_before: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index to insert items before',
|
||||
},
|
||||
range_length: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 1,
|
||||
description: 'Number of items to reorder',
|
||||
},
|
||||
snapshot_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Playlist snapshot ID for concurrency control (22-character base62 string)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
range_start: params.range_start,
|
||||
insert_before: params.insert_before,
|
||||
range_length: params.range_length || 1,
|
||||
}
|
||||
if (params.snapshot_id) body.snapshot_id = params.snapshot_id
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyReorderPlaylistItemsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: { snapshot_id: data.snapshot_id || '' },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyReplacePlaylistItemsParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
uris: string
|
||||
}
|
||||
|
||||
interface SpotifyReplacePlaylistItemsResponse extends ToolResponse {
|
||||
output: {
|
||||
snapshot_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyReplacePlaylistItemsTool: ToolConfig<
|
||||
SpotifyReplacePlaylistItemsParams,
|
||||
SpotifyReplacePlaylistItemsResponse
|
||||
> = {
|
||||
id: 'spotify_replace_playlist_items',
|
||||
name: 'Spotify Replace Playlist Items',
|
||||
description: 'Replace all items in a playlist with new tracks.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
uris: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated Spotify URIs (max 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/tracks`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
uris: params.uris
|
||||
.split(',')
|
||||
.map((uri) => uri.trim())
|
||||
.slice(0, 100),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyReplacePlaylistItemsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: { snapshot_id: data.snapshot_id || '' },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySaveAlbumsParams {
|
||||
accessToken: string
|
||||
albumIds: string
|
||||
}
|
||||
|
||||
interface SpotifySaveAlbumsResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifySaveAlbumsTool: ToolConfig<SpotifySaveAlbumsParams, SpotifySaveAlbumsResponse> =
|
||||
{
|
||||
id: 'spotify_save_albums',
|
||||
name: 'Spotify Save Albums',
|
||||
description: "Save albums to the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
albumIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated album IDs (max 20)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/albums',
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
ids: params.albumIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 20),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveAlbumsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether albums were saved' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySaveAudiobooksParams {
|
||||
accessToken: string
|
||||
audiobookIds: string
|
||||
}
|
||||
|
||||
interface SpotifySaveAudiobooksResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifySaveAudiobooksTool: ToolConfig<
|
||||
SpotifySaveAudiobooksParams,
|
||||
SpotifySaveAudiobooksResponse
|
||||
> = {
|
||||
id: 'spotify_save_audiobooks',
|
||||
name: 'Spotify Save Audiobooks',
|
||||
description: "Save audiobooks to the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
audiobookIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated audiobook IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.audiobookIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/audiobooks?ids=${ids}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveAudiobooksResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether audiobooks were saved' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySaveEpisodesParams {
|
||||
accessToken: string
|
||||
episodeIds: string
|
||||
}
|
||||
|
||||
interface SpotifySaveEpisodesResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifySaveEpisodesTool: ToolConfig<
|
||||
SpotifySaveEpisodesParams,
|
||||
SpotifySaveEpisodesResponse
|
||||
> = {
|
||||
id: 'spotify_save_episodes',
|
||||
name: 'Spotify Save Episodes',
|
||||
description: "Save podcast episodes to the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
episodeIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated episode IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.episodeIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/episodes?ids=${ids}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveEpisodesResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether episodes were saved' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySaveShowsParams {
|
||||
accessToken: string
|
||||
showIds: string
|
||||
}
|
||||
|
||||
interface SpotifySaveShowsResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifySaveShowsTool: ToolConfig<SpotifySaveShowsParams, SpotifySaveShowsResponse> = {
|
||||
id: 'spotify_save_shows',
|
||||
name: 'Spotify Save Shows',
|
||||
description: "Save podcast shows to the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated show IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.showIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/shows?ids=${ids}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveShowsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether shows were saved' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { SpotifySaveTracksParams, SpotifySaveTracksResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifySaveTracksTool: ToolConfig<SpotifySaveTracksParams, SpotifySaveTracksResponse> =
|
||||
{
|
||||
id: 'spotify_save_tracks',
|
||||
name: 'Spotify Save Tracks',
|
||||
description: "Save tracks to the current user's library (like tracks).",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
trackIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated Spotify track IDs to save (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.spotify.com/v1/me/tracks?ids=${encodeURIComponent(params.trackIds)}`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveTracksResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the tracks were saved successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { SpotifySearchParams, SpotifySearchResponse } from '@/tools/spotify/types'
|
||||
import {
|
||||
SEARCH_ALBUM_OUTPUT_PROPERTIES,
|
||||
SEARCH_ARTIST_OUTPUT_PROPERTIES,
|
||||
SEARCH_PLAYLIST_OUTPUT_PROPERTIES,
|
||||
SEARCH_TRACK_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifySearchTool: ToolConfig<SpotifySearchParams, SpotifySearchResponse> = {
|
||||
id: 'spotify_search',
|
||||
name: 'Spotify Search',
|
||||
description:
|
||||
'Search for tracks, albums, artists, or playlists on Spotify. Returns matching results based on the query.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query (e.g., "Bohemian Rhapsody", "artist:Queen", "genre:rock")',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 'track',
|
||||
description:
|
||||
'Type of results: track, album, artist, playlist, or comma-separated (e.g., "track,artist")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Maximum number of results to return (1-50)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 0,
|
||||
description: 'Index of the first result to return for pagination',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO 3166-1 alpha-2 country code to filter results (e.g., "US", "GB")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const type = params.type || 'track'
|
||||
const limit = Math.min(Math.max(params.limit || 20, 1), 50)
|
||||
const offset = params.offset || 0
|
||||
let url = `https://api.spotify.com/v1/search?q=${encodeURIComponent(params.query)}&type=${encodeURIComponent(type)}&limit=${limit}&offset=${offset}`
|
||||
if (params.market) {
|
||||
url += `&market=${params.market}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifySearchResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.tracks?.items || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => a.name) || [],
|
||||
album: track.album?.name || '',
|
||||
duration_ms: track.duration_ms,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
const artists = (data.artists?.items || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
const albums = (data.albums?.items || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => a.name) || [],
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
const playlists = (data.playlists?.items || []).map((playlist: any) => ({
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
owner: playlist.owner?.display_name || '',
|
||||
total_tracks: playlist.tracks?.total || 0,
|
||||
image_url: playlist.images?.[0]?.url || null,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
artists,
|
||||
albums,
|
||||
playlists,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of matching tracks',
|
||||
items: { type: 'object', properties: SEARCH_TRACK_OUTPUT_PROPERTIES },
|
||||
},
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: 'List of matching artists',
|
||||
items: { type: 'object', properties: SEARCH_ARTIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
albums: {
|
||||
type: 'array',
|
||||
description: 'List of matching albums',
|
||||
items: { type: 'object', properties: SEARCH_ALBUM_OUTPUT_PROPERTIES },
|
||||
},
|
||||
playlists: {
|
||||
type: 'array',
|
||||
description: 'List of matching playlists',
|
||||
items: { type: 'object', properties: SEARCH_PLAYLIST_OUTPUT_PROPERTIES },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySeekParams {
|
||||
accessToken: string
|
||||
position_ms: number
|
||||
device_id?: string
|
||||
}
|
||||
|
||||
interface SpotifySeekResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifySeekTool: ToolConfig<SpotifySeekParams, SpotifySeekResponse> = {
|
||||
id: 'spotify_seek',
|
||||
name: 'Spotify Seek',
|
||||
description: 'Seek to a position in the currently playing track.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
position_ms: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position in milliseconds to seek to',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Spotify device ID to target for playback',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/me/player/seek?position_ms=${params.position_ms}`
|
||||
if (params.device_id) {
|
||||
url += `&device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySeekResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether seek was successful' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySetRepeatParams {
|
||||
accessToken: string
|
||||
state: string
|
||||
device_id?: string
|
||||
}
|
||||
|
||||
interface SpotifySetRepeatResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifySetRepeatTool: ToolConfig<SpotifySetRepeatParams, SpotifySetRepeatResponse> = {
|
||||
id: 'spotify_set_repeat',
|
||||
name: 'Spotify Set Repeat',
|
||||
description: 'Set the repeat mode for playback.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
state: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Repeat mode: "off", "track", or "context"',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Spotify device ID to target for playback',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/me/player/repeat?state=${params.state}`
|
||||
if (params.device_id) {
|
||||
url += `&device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySetRepeatResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether repeat mode was set successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySetShuffleParams {
|
||||
accessToken: string
|
||||
state: boolean
|
||||
device_id?: string
|
||||
}
|
||||
|
||||
interface SpotifySetShuffleResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifySetShuffleTool: ToolConfig<SpotifySetShuffleParams, SpotifySetShuffleResponse> =
|
||||
{
|
||||
id: 'spotify_set_shuffle',
|
||||
name: 'Spotify Set Shuffle',
|
||||
description: 'Turn shuffle on or off.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
state: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'true for shuffle on, false for off',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Spotify device ID to target for playback',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/me/player/shuffle?state=${params.state}`
|
||||
if (params.device_id) {
|
||||
url += `&device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySetShuffleResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether shuffle was set successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { SpotifySetVolumeParams, SpotifySetVolumeResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifySetVolumeTool: ToolConfig<SpotifySetVolumeParams, SpotifySetVolumeResponse> = {
|
||||
id: 'spotify_set_volume',
|
||||
name: 'Spotify Set Volume',
|
||||
description: 'Set the playback volume on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
volume_percent: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Volume level (0 to 100)',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID. If not provided, uses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const volume = Math.min(Math.max(params.volume_percent, 0), 100)
|
||||
let url = `https://api.spotify.com/v1/me/player/volume?volume_percent=${volume}`
|
||||
if (params.device_id) {
|
||||
url += `&device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySetVolumeResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether volume was set' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { SpotifySkipNextParams, SpotifySkipNextResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifySkipNextTool: ToolConfig<SpotifySkipNextParams, SpotifySkipNextResponse> = {
|
||||
id: 'spotify_skip_next',
|
||||
name: 'Spotify Skip to Next',
|
||||
description: 'Skip to the next track on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID. If not provided, uses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player/next'
|
||||
if (params.device_id) {
|
||||
url += `?device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySkipNextResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether skip was successful' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { SpotifySkipPreviousParams, SpotifySkipPreviousResponse } from '@/tools/spotify/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const spotifySkipPreviousTool: ToolConfig<
|
||||
SpotifySkipPreviousParams,
|
||||
SpotifySkipPreviousResponse
|
||||
> = {
|
||||
id: 'spotify_skip_previous',
|
||||
name: 'Spotify Skip to Previous',
|
||||
description: 'Skip to the previous track on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID. If not provided, uses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player/previous'
|
||||
if (params.device_id) {
|
||||
url += `?device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySkipPreviousResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether skip was successful' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyTransferPlaybackParams {
|
||||
accessToken: string
|
||||
device_id: string
|
||||
play?: boolean
|
||||
}
|
||||
|
||||
interface SpotifyTransferPlaybackResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyTransferPlaybackTool: ToolConfig<
|
||||
SpotifyTransferPlaybackParams,
|
||||
SpotifyTransferPlaybackResponse
|
||||
> = {
|
||||
id: 'spotify_transfer_playback',
|
||||
name: 'Spotify Transfer Playback',
|
||||
description: 'Transfer playback to a different device.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID to transfer playback to',
|
||||
},
|
||||
play: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: true,
|
||||
description: 'Whether to start playing on the new device',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/player',
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
device_ids: [params.device_id],
|
||||
play: params.play ?? true,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyTransferPlaybackResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether transfer was successful' },
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyUnfollowArtistsParams {
|
||||
accessToken: string
|
||||
artistIds: string
|
||||
}
|
||||
|
||||
interface SpotifyUnfollowArtistsResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyUnfollowArtistsTool: ToolConfig<
|
||||
SpotifyUnfollowArtistsParams,
|
||||
SpotifyUnfollowArtistsResponse
|
||||
> = {
|
||||
id: 'spotify_unfollow_artists',
|
||||
name: 'Spotify Unfollow Artists',
|
||||
description: 'Unfollow one or more artists.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-follow-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
artistIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated artist IDs to unfollow (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.artistIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/following?type=artist&ids=${ids}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyUnfollowArtistsResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether artists were unfollowed successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyUnfollowPlaylistParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
}
|
||||
|
||||
interface SpotifyUnfollowPlaylistResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyUnfollowPlaylistTool: ToolConfig<
|
||||
SpotifyUnfollowPlaylistParams,
|
||||
SpotifyUnfollowPlaylistResponse
|
||||
> = {
|
||||
id: 'spotify_unfollow_playlist',
|
||||
name: 'Spotify Unfollow Playlist',
|
||||
description: 'Unfollow (unsave) a playlist.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}/followers`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyUnfollowPlaylistResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether unfollow succeeded' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyUpdatePlaylistParams {
|
||||
accessToken: string
|
||||
playlistId: string
|
||||
name?: string
|
||||
description?: string
|
||||
public?: boolean
|
||||
}
|
||||
|
||||
interface SpotifyUpdatePlaylistResponse extends ToolResponse {
|
||||
output: { success: boolean }
|
||||
}
|
||||
|
||||
export const spotifyUpdatePlaylistTool: ToolConfig<
|
||||
SpotifyUpdatePlaylistParams,
|
||||
SpotifyUpdatePlaylistResponse
|
||||
> = {
|
||||
id: 'spotify_update_playlist',
|
||||
name: 'Spotify Update Playlist',
|
||||
description: "Update a playlist's name, description, or visibility.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['playlist-modify-public', 'playlist-modify-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
playlistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify playlist ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New name for the playlist',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New description for the playlist',
|
||||
},
|
||||
public: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the playlist should be public',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/playlists/${params.playlistId}`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
if (params.name !== undefined) body.name = params.name
|
||||
if (params.description !== undefined) body.description = params.description
|
||||
if (params.public !== undefined) body.public = params.public
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyUpdatePlaylistResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether update succeeded' },
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user