From ecf5209e6f4d99732e0cad63f2cca8e43c8390b4 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:22:17 -0800 Subject: [PATCH] feat(integration): add spotify (#2347) * Add spotify * Finish spotify integration * Fix logo * fix build * Rename tools * Fix docs * Fix lint * Fix imports * ran lint --------- Co-authored-by: waleed --- apps/docs/components/icons.tsx | 12 + apps/docs/components/ui/icon-mapping.ts | 206 +-- apps/docs/content/docs/en/tools/grafana.mdx | 14 +- apps/docs/content/docs/en/tools/meta.json | 1 + apps/docs/content/docs/en/tools/spotify.mdx | 1456 +++++++++++++++++ apps/docs/content/docs/en/tools/zep.mdx | 11 +- apps/sim/blocks/blocks/spotify.ts | 1355 +++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 12 + apps/sim/lib/auth/auth.ts | 67 + apps/sim/lib/core/config/env.ts | 2 + .../sim/lib/logs/execution/logging-session.ts | 44 +- apps/sim/lib/oauth/oauth.ts | 52 + apps/sim/package.json | 2 +- apps/sim/tools/registry.ts | 159 ++ apps/sim/tools/spotify/add_playlist_cover.ts | 58 + apps/sim/tools/spotify/add_to_queue.ts | 59 + .../tools/spotify/add_tracks_to_playlist.ts | 71 + apps/sim/tools/spotify/check_following.ts | 64 + .../tools/spotify/check_playlist_followers.ts | 64 + apps/sim/tools/spotify/check_saved_albums.ts | 58 + .../tools/spotify/check_saved_audiobooks.ts | 58 + .../sim/tools/spotify/check_saved_episodes.ts | 58 + apps/sim/tools/spotify/check_saved_shows.ts | 58 + apps/sim/tools/spotify/check_saved_tracks.ts | 71 + apps/sim/tools/spotify/create_playlist.ts | 89 + apps/sim/tools/spotify/follow_artists.ts | 64 + apps/sim/tools/spotify/follow_playlist.ts | 61 + apps/sim/tools/spotify/get_album.ts | 87 + apps/sim/tools/spotify/get_album_tracks.ts | 112 ++ apps/sim/tools/spotify/get_albums.ts | 94 ++ apps/sim/tools/spotify/get_artist.ts | 59 + apps/sim/tools/spotify/get_artist_albums.ts | 116 ++ .../tools/spotify/get_artist_top_tracks.ts | 89 + apps/sim/tools/spotify/get_artists.ts | 82 + apps/sim/tools/spotify/get_audiobook.ts | 95 ++ .../tools/spotify/get_audiobook_chapters.ts | 104 ++ apps/sim/tools/spotify/get_audiobooks.ts | 87 + apps/sim/tools/spotify/get_categories.ts | 82 + apps/sim/tools/spotify/get_current_user.ts | 58 + .../tools/spotify/get_currently_playing.ts | 108 ++ apps/sim/tools/spotify/get_devices.ts | 67 + apps/sim/tools/spotify/get_episode.ts | 94 ++ apps/sim/tools/spotify/get_episodes.ts | 91 ++ .../sim/tools/spotify/get_followed_artists.ts | 100 ++ apps/sim/tools/spotify/get_markets.ts | 47 + apps/sim/tools/spotify/get_new_releases.ts | 88 + apps/sim/tools/spotify/get_playback_state.ts | 103 ++ apps/sim/tools/spotify/get_playlist.ts | 83 + apps/sim/tools/spotify/get_playlist_cover.ts | 66 + apps/sim/tools/spotify/get_playlist_tracks.ts | 113 ++ apps/sim/tools/spotify/get_queue.ts | 85 + apps/sim/tools/spotify/get_recently_played.ts | 102 ++ apps/sim/tools/spotify/get_saved_albums.ts | 106 ++ .../sim/tools/spotify/get_saved_audiobooks.ts | 97 ++ apps/sim/tools/spotify/get_saved_episodes.ts | 106 ++ apps/sim/tools/spotify/get_saved_shows.ts | 96 ++ apps/sim/tools/spotify/get_saved_tracks.ts | 104 ++ apps/sim/tools/spotify/get_show.ts | 89 + apps/sim/tools/spotify/get_show_episodes.ts | 106 ++ apps/sim/tools/spotify/get_shows.ts | 84 + apps/sim/tools/spotify/get_top_artists.ts | 100 ++ apps/sim/tools/spotify/get_top_tracks.ts | 104 ++ apps/sim/tools/spotify/get_track.ts | 81 + apps/sim/tools/spotify/get_tracks.ts | 94 ++ apps/sim/tools/spotify/get_user_playlists.ts | 96 ++ apps/sim/tools/spotify/get_user_profile.ts | 70 + apps/sim/tools/spotify/index.ts | 92 ++ apps/sim/tools/spotify/pause.ts | 52 + apps/sim/tools/spotify/play.ts | 94 ++ apps/sim/tools/spotify/remove_saved_albums.ts | 57 + .../tools/spotify/remove_saved_audiobooks.ts | 57 + .../tools/spotify/remove_saved_episodes.ts | 57 + apps/sim/tools/spotify/remove_saved_shows.ts | 57 + apps/sim/tools/spotify/remove_saved_tracks.ts | 63 + .../spotify/remove_tracks_from_playlist.ts | 65 + .../tools/spotify/reorder_playlist_items.ts | 91 ++ .../tools/spotify/replace_playlist_items.ts | 69 + apps/sim/tools/spotify/save_albums.ts | 55 + apps/sim/tools/spotify/save_audiobooks.ts | 57 + apps/sim/tools/spotify/save_episodes.ts | 57 + apps/sim/tools/spotify/save_shows.ts | 54 + apps/sim/tools/spotify/save_tracks.ts | 48 + apps/sim/tools/spotify/search.ts | 157 ++ apps/sim/tools/spotify/seek.ts | 67 + apps/sim/tools/spotify/set_repeat.ts | 67 + apps/sim/tools/spotify/set_shuffle.ts | 68 + apps/sim/tools/spotify/set_volume.ts | 59 + apps/sim/tools/spotify/skip_next.ts | 52 + apps/sim/tools/spotify/skip_previous.ts | 55 + apps/sim/tools/spotify/transfer_playback.ts | 69 + apps/sim/tools/spotify/types.ts | 1031 ++++++++++++ apps/sim/tools/spotify/unfollow_artists.ts | 64 + apps/sim/tools/spotify/unfollow_playlist.ts | 50 + apps/sim/tools/spotify/update_playlist.ts | 76 + 95 files changed, 10488 insertions(+), 125 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/spotify.mdx create mode 100644 apps/sim/blocks/blocks/spotify.ts create mode 100644 apps/sim/tools/spotify/add_playlist_cover.ts create mode 100644 apps/sim/tools/spotify/add_to_queue.ts create mode 100644 apps/sim/tools/spotify/add_tracks_to_playlist.ts create mode 100644 apps/sim/tools/spotify/check_following.ts create mode 100644 apps/sim/tools/spotify/check_playlist_followers.ts create mode 100644 apps/sim/tools/spotify/check_saved_albums.ts create mode 100644 apps/sim/tools/spotify/check_saved_audiobooks.ts create mode 100644 apps/sim/tools/spotify/check_saved_episodes.ts create mode 100644 apps/sim/tools/spotify/check_saved_shows.ts create mode 100644 apps/sim/tools/spotify/check_saved_tracks.ts create mode 100644 apps/sim/tools/spotify/create_playlist.ts create mode 100644 apps/sim/tools/spotify/follow_artists.ts create mode 100644 apps/sim/tools/spotify/follow_playlist.ts create mode 100644 apps/sim/tools/spotify/get_album.ts create mode 100644 apps/sim/tools/spotify/get_album_tracks.ts create mode 100644 apps/sim/tools/spotify/get_albums.ts create mode 100644 apps/sim/tools/spotify/get_artist.ts create mode 100644 apps/sim/tools/spotify/get_artist_albums.ts create mode 100644 apps/sim/tools/spotify/get_artist_top_tracks.ts create mode 100644 apps/sim/tools/spotify/get_artists.ts create mode 100644 apps/sim/tools/spotify/get_audiobook.ts create mode 100644 apps/sim/tools/spotify/get_audiobook_chapters.ts create mode 100644 apps/sim/tools/spotify/get_audiobooks.ts create mode 100644 apps/sim/tools/spotify/get_categories.ts create mode 100644 apps/sim/tools/spotify/get_current_user.ts create mode 100644 apps/sim/tools/spotify/get_currently_playing.ts create mode 100644 apps/sim/tools/spotify/get_devices.ts create mode 100644 apps/sim/tools/spotify/get_episode.ts create mode 100644 apps/sim/tools/spotify/get_episodes.ts create mode 100644 apps/sim/tools/spotify/get_followed_artists.ts create mode 100644 apps/sim/tools/spotify/get_markets.ts create mode 100644 apps/sim/tools/spotify/get_new_releases.ts create mode 100644 apps/sim/tools/spotify/get_playback_state.ts create mode 100644 apps/sim/tools/spotify/get_playlist.ts create mode 100644 apps/sim/tools/spotify/get_playlist_cover.ts create mode 100644 apps/sim/tools/spotify/get_playlist_tracks.ts create mode 100644 apps/sim/tools/spotify/get_queue.ts create mode 100644 apps/sim/tools/spotify/get_recently_played.ts create mode 100644 apps/sim/tools/spotify/get_saved_albums.ts create mode 100644 apps/sim/tools/spotify/get_saved_audiobooks.ts create mode 100644 apps/sim/tools/spotify/get_saved_episodes.ts create mode 100644 apps/sim/tools/spotify/get_saved_shows.ts create mode 100644 apps/sim/tools/spotify/get_saved_tracks.ts create mode 100644 apps/sim/tools/spotify/get_show.ts create mode 100644 apps/sim/tools/spotify/get_show_episodes.ts create mode 100644 apps/sim/tools/spotify/get_shows.ts create mode 100644 apps/sim/tools/spotify/get_top_artists.ts create mode 100644 apps/sim/tools/spotify/get_top_tracks.ts create mode 100644 apps/sim/tools/spotify/get_track.ts create mode 100644 apps/sim/tools/spotify/get_tracks.ts create mode 100644 apps/sim/tools/spotify/get_user_playlists.ts create mode 100644 apps/sim/tools/spotify/get_user_profile.ts create mode 100644 apps/sim/tools/spotify/index.ts create mode 100644 apps/sim/tools/spotify/pause.ts create mode 100644 apps/sim/tools/spotify/play.ts create mode 100644 apps/sim/tools/spotify/remove_saved_albums.ts create mode 100644 apps/sim/tools/spotify/remove_saved_audiobooks.ts create mode 100644 apps/sim/tools/spotify/remove_saved_episodes.ts create mode 100644 apps/sim/tools/spotify/remove_saved_shows.ts create mode 100644 apps/sim/tools/spotify/remove_saved_tracks.ts create mode 100644 apps/sim/tools/spotify/remove_tracks_from_playlist.ts create mode 100644 apps/sim/tools/spotify/reorder_playlist_items.ts create mode 100644 apps/sim/tools/spotify/replace_playlist_items.ts create mode 100644 apps/sim/tools/spotify/save_albums.ts create mode 100644 apps/sim/tools/spotify/save_audiobooks.ts create mode 100644 apps/sim/tools/spotify/save_episodes.ts create mode 100644 apps/sim/tools/spotify/save_shows.ts create mode 100644 apps/sim/tools/spotify/save_tracks.ts create mode 100644 apps/sim/tools/spotify/search.ts create mode 100644 apps/sim/tools/spotify/seek.ts create mode 100644 apps/sim/tools/spotify/set_repeat.ts create mode 100644 apps/sim/tools/spotify/set_shuffle.ts create mode 100644 apps/sim/tools/spotify/set_volume.ts create mode 100644 apps/sim/tools/spotify/skip_next.ts create mode 100644 apps/sim/tools/spotify/skip_previous.ts create mode 100644 apps/sim/tools/spotify/transfer_playback.ts create mode 100644 apps/sim/tools/spotify/types.ts create mode 100644 apps/sim/tools/spotify/unfollow_artists.ts create mode 100644 apps/sim/tools/spotify/unfollow_playlist.ts create mode 100644 apps/sim/tools/spotify/update_playlist.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index e88000cab..f9e690a72 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4203,3 +4203,15 @@ export function RssIcon(props: SVGProps) { ) } + +export function SpotifyIcon(props: SVGProps) { + return ( + + + + + ) +} diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 6476bf073..977138187 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -89,6 +89,7 @@ import { ShopifyIcon, SlackIcon, SmtpIcon, + SpotifyIcon, SQSIcon, SshIcon, STTIcon, @@ -118,115 +119,116 @@ import { type IconComponent = ComponentType> export const blockTypeToIconMap: Record = { - zoom: ZoomIcon, - zep: ZepIcon, + calendly: CalendlyIcon, + mailchimp: MailchimpIcon, + postgresql: PostgresIcon, + twilio_voice: TwilioIcon, + elasticsearch: ElasticsearchIcon, + rds: RDSIcon, + translate: TranslateIcon, + dynamodb: DynamoDBIcon, + wordpress: WordpressIcon, + tavily: TavilyIcon, zendesk: ZendeskIcon, youtube: YouTubeIcon, - x: xIcon, - wordpress: WordpressIcon, - wikipedia: WikipediaIcon, - whatsapp: WhatsAppIcon, - webflow: WebflowIcon, - wealthbox: WealthboxIcon, - vision: EyeIcon, - video_generator: VideoIcon, - typeform: TypeformIcon, - twilio_voice: TwilioIcon, - twilio_sms: TwilioIcon, - tts: TTSIcon, - trello: TrelloIcon, - translate: TranslateIcon, - thinking: BrainIcon, - telegram: TelegramIcon, - tavily: TavilyIcon, supabase: SupabaseIcon, - stt: STTIcon, - stripe: StripeIcon, - stagehand: StagehandIcon, - ssh: SshIcon, - sqs: SQSIcon, - smtp: SmtpIcon, - slack: SlackIcon, - shopify: ShopifyIcon, - sharepoint: MicrosoftSharepointIcon, - sftp: SftpIcon, - serper: SerperIcon, - sentry: SentryIcon, - sendgrid: SendgridIcon, - search: SearchIcon, - salesforce: SalesforceIcon, - s3: S3Icon, - resend: ResendIcon, - reddit: RedditIcon, - rds: RDSIcon, - qdrant: QdrantIcon, - posthog: PosthogIcon, - postgresql: PostgresIcon, - polymarket: PolymarketIcon, - pipedrive: PipedriveIcon, - pinecone: PineconeIcon, - perplexity: PerplexityIcon, - parallel_ai: ParallelIcon, - outlook: OutlookIcon, - openai: OpenAIIcon, - onedrive: MicrosoftOneDriveIcon, - notion: NotionIcon, - neo4j: Neo4jIcon, - mysql: MySQLIcon, - mongodb: MongoDBIcon, - mistral_parse: MistralIcon, - microsoft_teams: MicrosoftTeamsIcon, - microsoft_planner: MicrosoftPlannerIcon, - microsoft_excel: MicrosoftExcelIcon, - memory: BrainIcon, - mem0: Mem0Icon, - mailgun: MailgunIcon, - mailchimp: MailchimpIcon, - linkup: LinkupIcon, - linkedin: LinkedInIcon, - linear: LinearIcon, - knowledge: PackageSearchIcon, - kalshi: KalshiIcon, - jira: JiraIcon, - jina: JinaAIIcon, - intercom: IntercomIcon, - incidentio: IncidentioIcon, - image_generator: ImageIcon, - hunter: HunterIOIcon, - huggingface: HuggingFaceIcon, - hubspot: HubspotIcon, - grafana: GrafanaIcon, - google_vault: GoogleVaultIcon, - google_slides: GoogleSlidesIcon, - google_sheets: GoogleSheetsIcon, - google_groups: GoogleGroupsIcon, - google_forms: GoogleFormsIcon, - google_drive: GoogleDriveIcon, - google_docs: GoogleDocsIcon, - google_calendar: GoogleCalendarIcon, - google_search: GoogleIcon, - gmail: GmailIcon, - gitlab: GitLabIcon, - github: GithubIcon, - firecrawl: FirecrawlIcon, - file: DocumentIcon, - exa: ExaAIIcon, - elevenlabs: ElevenLabsIcon, - elasticsearch: ElasticsearchIcon, - dynamodb: DynamoDBIcon, - duckduckgo: DuckDuckGoIcon, - dropbox: DropboxIcon, - discord: DiscordIcon, - datadog: DatadogIcon, - cursor: CursorIcon, + vision: EyeIcon, + zoom: ZoomIcon, confluence: ConfluenceIcon, - clay: ClayIcon, - calendly: CalendlyIcon, - browser_use: BrowserUseIcon, - asana: AsanaIcon, arxiv: ArxivIcon, + webflow: WebflowIcon, + pinecone: PineconeIcon, apollo: ApolloIcon, + whatsapp: WhatsAppIcon, + typeform: TypeformIcon, + qdrant: QdrantIcon, + shopify: ShopifyIcon, + asana: AsanaIcon, + sqs: SQSIcon, apify: ApifyIcon, + memory: BrainIcon, + gitlab: GitLabIcon, + polymarket: PolymarketIcon, + serper: SerperIcon, + linear: LinearIcon, + exa: ExaAIIcon, + telegram: TelegramIcon, + salesforce: SalesforceIcon, + hubspot: HubspotIcon, + hunter: HunterIOIcon, + linkup: LinkupIcon, + mongodb: MongoDBIcon, airtable: AirtableIcon, + discord: DiscordIcon, ahrefs: AhrefsIcon, + neo4j: Neo4jIcon, + tts: TTSIcon, + jina: JinaAIIcon, + google_docs: GoogleDocsIcon, + perplexity: PerplexityIcon, + google_search: GoogleIcon, + x: xIcon, + kalshi: KalshiIcon, + google_calendar: GoogleCalendarIcon, + zep: ZepIcon, + posthog: PosthogIcon, + grafana: GrafanaIcon, + google_slides: GoogleSlidesIcon, + microsoft_planner: MicrosoftPlannerIcon, + thinking: BrainIcon, + pipedrive: PipedriveIcon, + dropbox: DropboxIcon, + stagehand: StagehandIcon, + google_forms: GoogleFormsIcon, + file: DocumentIcon, + mistral_parse: MistralIcon, + gmail: GmailIcon, + openai: OpenAIIcon, + outlook: OutlookIcon, + incidentio: IncidentioIcon, + onedrive: MicrosoftOneDriveIcon, + resend: ResendIcon, + google_vault: GoogleVaultIcon, + sharepoint: MicrosoftSharepointIcon, + huggingface: HuggingFaceIcon, + sendgrid: SendgridIcon, + video_generator: VideoIcon, + smtp: SmtpIcon, + google_groups: GoogleGroupsIcon, + mailgun: MailgunIcon, + clay: ClayIcon, + jira: JiraIcon, + search: SearchIcon, + linkedin: LinkedInIcon, + wealthbox: WealthboxIcon, + notion: NotionIcon, + elevenlabs: ElevenLabsIcon, + microsoft_teams: MicrosoftTeamsIcon, + github: GithubIcon, + sftp: SftpIcon, + ssh: SshIcon, + google_drive: GoogleDriveIcon, + sentry: SentryIcon, + reddit: RedditIcon, + parallel_ai: ParallelIcon, + spotify: SpotifyIcon, + stripe: StripeIcon, + s3: S3Icon, + trello: TrelloIcon, + mem0: Mem0Icon, + knowledge: PackageSearchIcon, + intercom: IntercomIcon, + twilio_sms: TwilioIcon, + duckduckgo: DuckDuckGoIcon, + slack: SlackIcon, + datadog: DatadogIcon, + microsoft_excel: MicrosoftExcelIcon, + image_generator: ImageIcon, + google_sheets: GoogleSheetsIcon, + wikipedia: WikipediaIcon, + cursor: CursorIcon, + firecrawl: FirecrawlIcon, + mysql: MySQLIcon, + browser_use: BrowserUseIcon, + stt: STTIcon, } diff --git a/apps/docs/content/docs/en/tools/grafana.mdx b/apps/docs/content/docs/en/tools/grafana.mdx index c79e5c37c..6654b1c12 100644 --- a/apps/docs/content/docs/en/tools/grafana.mdx +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -324,7 +324,7 @@ Create an annotation on a dashboard or as a global annotation | `organizationId` | string | No | Organization ID for multi-org Grafana instances | | `text` | string | Yes | The text content of the annotation | | `tags` | string | No | Comma-separated list of tags | -| `dashboardUid` | string | No | UID of the dashboard to add the annotation to \(optional for global annotations\) | +| `dashboardUid` | string | Yes | UID of the dashboard to add the annotation to | | `panelId` | number | No | ID of the panel to add the annotation to | | `time` | number | No | Start time in epoch milliseconds \(defaults to now\) | | `timeEnd` | number | No | End time in epoch milliseconds \(for range annotations\) | @@ -349,7 +349,7 @@ Query annotations by time range, dashboard, or tags | `organizationId` | string | No | Organization ID for multi-org Grafana instances | | `from` | number | No | Start time in epoch milliseconds | | `to` | number | No | End time in epoch milliseconds | -| `dashboardUid` | string | No | Filter by dashboard UID | +| `dashboardUid` | string | Yes | Dashboard UID to query annotations from | | `panelId` | number | No | Filter by panel ID | | `tags` | string | No | Comma-separated list of tags to filter by | | `type` | string | No | Filter by type \(alert or annotation\) | @@ -490,6 +490,16 @@ Create a new folder in Grafana | `uid` | string | The UID of the created folder | | `title` | string | The title of the created folder | | `url` | string | The URL path to the folder | +| `hasAcl` | boolean | Whether the folder has custom ACL permissions | +| `canSave` | boolean | Whether the current user can save the folder | +| `canEdit` | boolean | Whether the current user can edit the folder | +| `canAdmin` | boolean | Whether the current user has admin rights on the folder | +| `canDelete` | boolean | Whether the current user can delete the folder | +| `createdBy` | string | Username of who created the folder | +| `created` | string | Timestamp when the folder was created | +| `updatedBy` | string | Username of who last updated the folder | +| `updated` | string | Timestamp when the folder was last updated | +| `version` | number | Version number of the folder | diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index b9c653d06..7ca67d7a9 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -85,6 +85,7 @@ "shopify", "slack", "smtp", + "spotify", "sqs", "ssh", "stagehand", diff --git a/apps/docs/content/docs/en/tools/spotify.mdx b/apps/docs/content/docs/en/tools/spotify.mdx new file mode 100644 index 000000000..1b8cd3b0c --- /dev/null +++ b/apps/docs/content/docs/en/tools/spotify.mdx @@ -0,0 +1,1456 @@ +--- +title: Spotify +description: Search music, manage playlists, control playback, and access your library +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Spotify into your workflow. Search for tracks, albums, artists, and playlists. Manage playlists, access your library, control playback, browse podcasts and audiobooks. + + + +## Tools + +### `spotify_search` + +Search for tracks, albums, artists, or playlists on Spotify. Returns matching results based on the query. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search query \(e.g., "Bohemian Rhapsody", "artist:Queen", "genre:rock"\) | +| `type` | string | No | Type of results: track, album, artist, playlist, or comma-separated \(e.g., "track,artist"\) | +| `limit` | number | No | Maximum number of results to return \(1-50\) | +| `offset` | number | No | Index of the first result to return for pagination | +| `market` | string | No | ISO 3166-1 alpha-2 country code to filter results \(e.g., "US", "GB"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tracks` | array | List of matching tracks | + +### `spotify_get_track` + +Get detailed information about a specific track on Spotify by its ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `trackId` | string | Yes | The Spotify ID of the track | +| `market` | string | No | ISO 3166-1 alpha-2 country code for track availability | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Spotify track ID | +| `name` | string | Track name | +| `artists` | array | List of artists | +| `album` | object | Album information | +| `duration_ms` | number | Track duration in milliseconds | +| `explicit` | boolean | Whether the track has explicit content | +| `popularity` | number | Popularity score \(0-100\) | +| `preview_url` | string | URL to 30-second preview | +| `external_url` | string | Spotify URL | +| `uri` | string | Spotify URI for the track | + +### `spotify_get_tracks` + +Get detailed information about multiple tracks on Spotify by their IDs (up to 50). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `trackIds` | string | Yes | Comma-separated list of Spotify track IDs \(max 50\) | +| `market` | string | No | ISO 3166-1 alpha-2 country code for track availability | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tracks` | array | List of tracks | + +### `spotify_get_album` + +Get detailed information about an album on Spotify by its ID, including track listing. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `albumId` | string | Yes | The Spotify ID of the album | +| `market` | string | No | ISO 3166-1 alpha-2 country code for track availability | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Spotify album ID | +| `name` | string | Album name | +| `artists` | array | List of artists | +| `album_type` | string | Type of album \(album, single, compilation\) | +| `total_tracks` | number | Total number of tracks | +| `release_date` | string | Release date | +| `label` | string | Record label | +| `popularity` | number | Popularity score \(0-100\) | +| `genres` | array | List of genres | +| `image_url` | string | Album cover image URL | +| `tracks` | array | List of tracks on the album | +| `external_url` | string | Spotify URL | + +### `spotify_get_albums` + +Get details for multiple albums by their IDs. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `albumIds` | string | Yes | Comma-separated album IDs \(max 20\) | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `albums` | json | List of albums | + +### `spotify_get_album_tracks` + +Get the tracks from an album. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `albumId` | string | Yes | The Spotify album ID | +| `limit` | number | No | Number of tracks to return \(1-50\) | +| `offset` | number | No | Index of first track to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tracks` | json | List of tracks | +| `total` | number | Total number of tracks | +| `next` | string | URL for next page | + +### `spotify_get_saved_albums` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of albums to return \(1-50\) | +| `offset` | number | No | Index of first album to return | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `albums` | json | List of saved albums | +| `total` | number | Total saved albums | +| `next` | string | URL for next page | + +### `spotify_save_albums` + +Save albums to the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `albumIds` | string | Yes | Comma-separated album IDs \(max 20\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether albums were saved | + +### `spotify_remove_saved_albums` + +Remove albums from the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `albumIds` | string | Yes | Comma-separated album IDs \(max 20\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether albums were removed | + +### `spotify_check_saved_albums` + +Check if albums are saved in library. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `albumIds` | string | Yes | Comma-separated album IDs \(max 20\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of booleans for each album | + +### `spotify_get_artist` + +Get detailed information about an artist on Spotify by their ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `artistId` | string | Yes | The Spotify ID of the artist | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Spotify artist ID | +| `name` | string | Artist name | +| `genres` | array | List of genres associated with the artist | +| `popularity` | number | Popularity score \(0-100\) | +| `followers` | number | Number of followers | +| `image_url` | string | Artist image URL | +| `external_url` | string | Spotify URL | + +### `spotify_get_artists` + +Get details for multiple artists by their IDs. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `artistIds` | string | Yes | Comma-separated artist IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `artists` | json | List of artists | + +### `spotify_get_artist_albums` + +Get albums by an artist on Spotify. Can filter by album type. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `artistId` | string | Yes | The Spotify ID of the artist | +| `include_groups` | string | No | Filter by album type: album, single, appears_on, compilation \(comma-separated\) | +| `limit` | number | No | Maximum number of albums to return \(1-50\) | +| `offset` | number | No | Index of the first album to return | +| `market` | string | No | ISO 3166-1 alpha-2 country code | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `albums` | array | Artist | + +### `spotify_get_artist_top_tracks` + +Get the top 10 most popular tracks by an artist on Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `artistId` | string | Yes | The Spotify ID of the artist | +| `market` | string | No | ISO 3166-1 alpha-2 country code \(required for this endpoint\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tracks` | array | Artist | + +### `spotify_follow_artists` + +Follow one or more artists. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `artistIds` | string | Yes | Comma-separated artist IDs to follow \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether artists were followed successfully | + +### `spotify_unfollow_artists` + +Unfollow one or more artists. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `artistIds` | string | Yes | Comma-separated artist IDs to unfollow \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether artists were unfollowed successfully | + +### `spotify_get_followed_artists` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of artists to return \(1-50\) | +| `after` | string | No | Cursor for pagination \(last artist ID from previous request\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `artists` | json | List of followed artists | +| `total` | number | Total number of followed artists | +| `next` | string | Cursor for next page | + +### `spotify_check_following` + +Check if the user follows artists or users. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `type` | string | Yes | Type to check: "artist" or "user" | +| `ids` | string | Yes | Comma-separated artist or user IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of booleans for each ID | + +### `spotify_get_show` + +Get details for a podcast show. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `showId` | string | Yes | The Spotify show ID | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Show ID | +| `name` | string | Show name | +| `description` | string | Show description | +| `publisher` | string | Publisher name | +| `total_episodes` | number | Total episodes | +| `explicit` | boolean | Contains explicit content | +| `languages` | json | Languages | +| `image_url` | string | Cover image URL | +| `external_url` | string | Spotify URL | + +### `spotify_get_shows` + +Get details for multiple podcast shows. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `showIds` | string | Yes | Comma-separated show IDs \(max 50\) | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `shows` | json | List of shows | + +### `spotify_get_show_episodes` + +Get episodes from a podcast show. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `showId` | string | Yes | The Spotify show ID | +| `limit` | number | No | Number of episodes to return \(1-50\) | +| `offset` | number | No | Index of first episode to return | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `episodes` | json | List of episodes | +| `total` | number | Total episodes | +| `next` | string | URL for next page | + +### `spotify_get_saved_shows` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of shows to return \(1-50\) | +| `offset` | number | No | Index of first show to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `shows` | json | List of saved shows | +| `total` | number | Total saved shows | +| `next` | string | URL for next page | + +### `spotify_save_shows` + +Save podcast shows to the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `showIds` | string | Yes | Comma-separated show IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether shows were saved | + +### `spotify_remove_saved_shows` + +Remove podcast shows from the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `showIds` | string | Yes | Comma-separated show IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether shows were removed | + +### `spotify_check_saved_shows` + +Check if shows are saved in library. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `showIds` | string | Yes | Comma-separated show IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of booleans for each show | + +### `spotify_get_episode` + +Get details for a podcast episode. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `episodeId` | string | Yes | The Spotify episode ID | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Episode ID | +| `name` | string | Episode name | +| `description` | string | Episode description | +| `duration_ms` | number | Duration in ms | +| `release_date` | string | Release date | +| `explicit` | boolean | Contains explicit content | +| `show` | json | Parent show info | +| `image_url` | string | Cover image URL | +| `external_url` | string | Spotify URL | + +### `spotify_get_episodes` + +Get details for multiple podcast episodes. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `episodeIds` | string | Yes | Comma-separated episode IDs \(max 50\) | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `episodes` | json | List of episodes | + +### `spotify_get_saved_episodes` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of episodes to return \(1-50\) | +| `offset` | number | No | Index of first episode to return | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `episodes` | json | List of saved episodes | +| `total` | number | Total saved episodes | +| `next` | string | URL for next page | + +### `spotify_save_episodes` + +Save podcast episodes to the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `episodeIds` | string | Yes | Comma-separated episode IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether episodes were saved | + +### `spotify_remove_saved_episodes` + +Remove podcast episodes from the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `episodeIds` | string | Yes | Comma-separated episode IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether episodes were removed | + +### `spotify_check_saved_episodes` + +Check if episodes are saved in library. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `episodeIds` | string | Yes | Comma-separated episode IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of booleans for each episode | + +### `spotify_get_audiobook` + +Get details for an audiobook. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audiobookId` | string | Yes | The Spotify audiobook ID | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Audiobook ID | +| `name` | string | Audiobook name | +| `authors` | json | Authors | +| `narrators` | json | Narrators | +| `publisher` | string | Publisher | +| `description` | string | Description | +| `total_chapters` | number | Total chapters | +| `languages` | json | Languages | +| `image_url` | string | Cover image URL | +| `external_url` | string | Spotify URL | + +### `spotify_get_audiobooks` + +Get details for multiple audiobooks. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audiobookIds` | string | Yes | Comma-separated audiobook IDs \(max 50\) | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `audiobooks` | json | List of audiobooks | + +### `spotify_get_audiobook_chapters` + +Get chapters from an audiobook. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audiobookId` | string | Yes | The Spotify audiobook ID | +| `limit` | number | No | Number of chapters to return \(1-50\) | +| `offset` | number | No | Index of first chapter to return | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `chapters` | json | List of chapters | +| `total` | number | Total chapters | +| `next` | string | URL for next page | + +### `spotify_get_saved_audiobooks` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of audiobooks to return \(1-50\) | +| `offset` | number | No | Index of first audiobook to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `audiobooks` | json | List of saved audiobooks | +| `total` | number | Total saved audiobooks | +| `next` | string | URL for next page | + +### `spotify_save_audiobooks` + +Save audiobooks to the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audiobookIds` | string | Yes | Comma-separated audiobook IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether audiobooks were saved | + +### `spotify_remove_saved_audiobooks` + +Remove audiobooks from the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audiobookIds` | string | Yes | Comma-separated audiobook IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether audiobooks were removed | + +### `spotify_check_saved_audiobooks` + +Check if audiobooks are saved in library. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audiobookIds` | string | Yes | Comma-separated audiobook IDs \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of booleans for each audiobook | + +### `spotify_get_playlist` + +Get detailed information about a playlist on Spotify by its ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify ID of the playlist | +| `market` | string | No | ISO 3166-1 alpha-2 country code for track availability | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Spotify playlist ID | +| `name` | string | Playlist name | +| `description` | string | Playlist description | +| `public` | boolean | Whether the playlist is public | +| `collaborative` | boolean | Whether the playlist is collaborative | +| `owner` | object | Playlist owner information | +| `image_url` | string | Playlist cover image URL | +| `total_tracks` | number | Total number of tracks | +| `snapshot_id` | string | Playlist snapshot ID for versioning | +| `external_url` | string | Spotify URL | + +### `spotify_get_playlist_tracks` + +Get the tracks in a Spotify playlist. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify ID of the playlist | +| `limit` | number | No | Maximum number of tracks to return \(1-100\) | +| `offset` | number | No | Index of the first track to return | +| `market` | string | No | ISO 3166-1 alpha-2 country code for track availability | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tracks` | array | List of tracks in the playlist | + +### `spotify_get_playlist_cover` + +Get a playlist + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `images` | json | List of cover images | + +### `spotify_get_user_playlists` + +Get the current user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Maximum number of playlists to return \(1-50\) | +| `offset` | number | No | Index of the first playlist to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `playlists` | array | User | + +### `spotify_create_playlist` + +Create a new playlist for the current user on Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Name for the new playlist | +| `description` | string | No | Description for the playlist | +| `public` | boolean | No | Whether the playlist should be public | +| `collaborative` | boolean | No | Whether the playlist should be collaborative \(requires public to be false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Spotify playlist ID | +| `name` | string | Playlist name | +| `description` | string | Playlist description | +| `public` | boolean | Whether the playlist is public | +| `collaborative` | boolean | Whether collaborative | +| `snapshot_id` | string | Playlist snapshot ID | +| `external_url` | string | Spotify URL | + +### `spotify_update_playlist` + +Update a playlist + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | +| `name` | string | No | New name for the playlist | +| `description` | string | No | New description for the playlist | +| `public` | boolean | No | Whether the playlist should be public | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether update succeeded | + +### `spotify_add_playlist_cover` + +Upload a custom cover image for a playlist. Image must be JPEG and under 256KB. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | +| `imageBase64` | string | Yes | Base64-encoded JPEG image \(max 256KB\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether upload succeeded | + +### `spotify_add_tracks_to_playlist` + +Add one or more tracks to a Spotify playlist. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify ID of the playlist | +| `uris` | string | Yes | Comma-separated Spotify URIs \(e.g., "spotify:track:xxx,spotify:track:yyy"\) | +| `position` | number | No | Position to insert tracks \(0-based\). If omitted, tracks are appended. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `snapshot_id` | string | New playlist snapshot ID after modification | + +### `spotify_remove_tracks_from_playlist` + +Remove one or more tracks from a Spotify playlist. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify ID of the playlist | +| `uris` | string | Yes | Comma-separated Spotify URIs to remove \(e.g., "spotify:track:xxx,spotify:track:yyy"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `snapshot_id` | string | New playlist snapshot ID after modification | + +### `spotify_reorder_playlist_items` + +Move tracks to a different position in a playlist. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | +| `range_start` | number | Yes | Start index of items to reorder | +| `insert_before` | number | Yes | Index to insert items before | +| `range_length` | number | No | Number of items to reorder | +| `snapshot_id` | string | No | Playlist snapshot ID for concurrency control | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `snapshot_id` | string | New playlist snapshot ID | + +### `spotify_replace_playlist_items` + +Replace all items in a playlist with new tracks. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | +| `uris` | string | Yes | Comma-separated Spotify URIs \(max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `snapshot_id` | string | New playlist snapshot ID | + +### `spotify_follow_playlist` + +Follow (save) a playlist. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | +| `public` | boolean | No | Whether the playlist will be in public playlists | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether follow succeeded | + +### `spotify_unfollow_playlist` + +Unfollow (unsave) a playlist. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether unfollow succeeded | + +### `spotify_check_playlist_followers` + +Check if users follow a playlist. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | The Spotify playlist ID | +| `userIds` | string | Yes | Comma-separated user IDs to check \(max 5\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of booleans for each user | + +### `spotify_get_current_user` + +Get the current user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Spotify user ID | +| `display_name` | string | Display name | +| `email` | string | Email address | +| `country` | string | Country code | +| `product` | string | Subscription level \(free, premium\) | +| `followers` | number | Number of followers | +| `image_url` | string | Profile image URL | +| `external_url` | string | Spotify profile URL | + +### `spotify_get_user_profile` + +Get a user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The Spotify user ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | User ID | +| `display_name` | string | Display name | +| `followers` | number | Number of followers | +| `image_url` | string | Profile image URL | +| `external_url` | string | Spotify URL | + +### `spotify_get_top_tracks` + +Get the current user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `time_range` | string | No | Time range: short_term \(~4 weeks\), medium_term \(~6 months\), long_term \(years\) | +| `limit` | number | No | Number of tracks to return \(1-50\) | +| `offset` | number | No | Index of the first track to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tracks` | array | User | + +### `spotify_get_top_artists` + +Get the current user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `time_range` | string | No | Time range: short_term \(~4 weeks\), medium_term \(~6 months\), long_term \(years\) | +| `limit` | number | No | Number of artists to return \(1-50\) | +| `offset` | number | No | Index of the first artist to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `artists` | array | User | + +### `spotify_get_saved_tracks` + +Get the current user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of tracks to return \(1-50\) | +| `offset` | number | No | Index of the first track to return | +| `market` | string | No | ISO 3166-1 alpha-2 country code | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tracks` | array | User | + +### `spotify_save_tracks` + +Save tracks to the current user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `trackIds` | string | Yes | Comma-separated Spotify track IDs to save \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the tracks were saved successfully | + +### `spotify_remove_saved_tracks` + +Remove tracks from the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `trackIds` | string | Yes | Comma-separated track IDs to remove \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether tracks were removed successfully | + +### `spotify_check_saved_tracks` + +Check if one or more tracks are saved in the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `trackIds` | string | Yes | Comma-separated track IDs to check \(max 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of track IDs with saved status | +| `all_saved` | boolean | Whether all tracks are saved | +| `none_saved` | boolean | Whether no tracks are saved | + +### `spotify_get_recently_played` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of tracks to return \(1-50\) | +| `after` | number | No | Unix timestamp in milliseconds. Returns items after this cursor. | +| `before` | number | No | Unix timestamp in milliseconds. Returns items before this cursor. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Recently played tracks | + +### `spotify_get_new_releases` + +Get a list of new album releases featured in Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `country` | string | No | ISO 3166-1 alpha-2 country code \(e.g., "US", "GB"\) | +| `limit` | number | No | Number of releases to return \(1-50\) | +| `offset` | number | No | Index of first release to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `albums` | json | List of new releases | +| `total` | number | Total number of new releases | +| `next` | string | URL for next page | + +### `spotify_get_categories` + +Get a list of browse categories used to tag items in Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `country` | string | No | ISO 3166-1 alpha-2 country code \(e.g., "US", "GB"\) | +| `locale` | string | No | Locale code \(e.g., "en_US", "es_MX"\) | +| `limit` | number | No | Number of categories to return \(1-50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `categories` | json | List of browse categories | +| `total` | number | Total number of categories | + +### `spotify_get_markets` + +Get the list of markets where Spotify is available. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `markets` | json | List of ISO country codes | + +### `spotify_get_playback_state` + +Get the current playback state including device, track, and progress. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `market` | string | No | ISO 3166-1 alpha-2 country code | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `is_playing` | boolean | Whether playback is active | +| `device` | object | Active device information | +| `progress_ms` | number | Progress in milliseconds | +| `currently_playing_type` | string | Type of content playing | +| `shuffle_state` | boolean | Whether shuffle is enabled | +| `repeat_state` | string | Repeat mode \(off, track, context\) | +| `track` | object | Currently playing track | + +### `spotify_get_currently_playing` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `market` | string | No | ISO country code for market | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `is_playing` | boolean | Whether playback is active | +| `progress_ms` | number | Current position in track \(ms\) | +| `track` | json | Currently playing track | + +### `spotify_get_devices` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `devices` | array | Available playback devices | + +### `spotify_get_queue` + +Get the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `currently_playing` | json | Currently playing track | +| `queue` | json | Upcoming tracks in queue | + +### `spotify_play` + +Start or resume playback on Spotify. Can play specific tracks, albums, or playlists. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `device_id` | string | No | Device ID to play on. If not provided, plays on active device. | +| `context_uri` | string | No | Spotify URI of album, artist, or playlist to play \(e.g., "spotify:album:xxx"\) | +| `uris` | string | No | Comma-separated track URIs to play \(e.g., "spotify:track:xxx,spotify:track:yyy"\) | +| `offset` | number | No | Position in context to start playing \(0-based index\) | +| `position_ms` | number | No | Position in track to start from \(in milliseconds\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether playback started successfully | + +### `spotify_pause` + +Pause playback on Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `device_id` | string | No | Device ID to pause. If not provided, pauses active device. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether playback was paused | + +### `spotify_skip_next` + +Skip to the next track on Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `device_id` | string | No | Device ID. If not provided, uses active device. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether skip was successful | + +### `spotify_skip_previous` + +Skip to the previous track on Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `device_id` | string | No | Device ID. If not provided, uses active device. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether skip was successful | + +### `spotify_seek` + +Seek to a position in the currently playing track. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `position_ms` | number | Yes | Position in milliseconds to seek to | +| `device_id` | string | No | Device ID to target | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether seek was successful | + +### `spotify_add_to_queue` + +Add a track to the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `uri` | string | Yes | Spotify URI of the track to add \(e.g., "spotify:track:xxx"\) | +| `device_id` | string | No | Device ID. If not provided, uses active device. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether track was added to queue | + +### `spotify_set_volume` + +Set the playback volume on Spotify. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `volume_percent` | number | Yes | Volume level \(0 to 100\) | +| `device_id` | string | No | Device ID. If not provided, uses active device. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether volume was set | + +### `spotify_set_repeat` + +Set the repeat mode for playback. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `state` | string | Yes | Repeat mode: "off", "track", or "context" | +| `device_id` | string | No | Device ID to target | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether repeat mode was set successfully | + +### `spotify_set_shuffle` + +Turn shuffle on or off. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `state` | boolean | Yes | true for shuffle on, false for off | +| `device_id` | string | No | Device ID to target | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether shuffle was set successfully | + +### `spotify_transfer_playback` + +Transfer playback to a different device. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `device_id` | string | Yes | Device ID to transfer playback to | +| `play` | boolean | No | Whether to start playing on the new device | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether transfer was successful | + + + +## Notes + +- Category: `tools` +- Type: `spotify` diff --git a/apps/docs/content/docs/en/tools/zep.mdx b/apps/docs/content/docs/en/tools/zep.mdx index 12b924a71..4b53ed8f4 100644 --- a/apps/docs/content/docs/en/tools/zep.mdx +++ b/apps/docs/content/docs/en/tools/zep.mdx @@ -96,10 +96,7 @@ Retrieve user context from a thread with summary or basic mode | Parameter | Type | Description | | --------- | ---- | ----------- | -| `context` | string | The context string \(summary or basic\) | -| `facts` | array | Extracted facts | -| `entities` | array | Extracted entities | -| `summary` | string | Conversation summary | +| `context` | string | The context string \(summary or basic mode\) | ### `zep_get_messages` @@ -139,9 +136,9 @@ Add messages to an existing thread | Parameter | Type | Description | | --------- | ---- | ----------- | -| `context` | string | Updated context after adding messages | -| `messageIds` | array | Array of added message UUIDs | | `threadId` | string | The thread ID | +| `added` | boolean | Whether messages were added successfully | +| `messageIds` | array | Array of added message UUIDs | ### `zep_add_user` @@ -211,7 +208,7 @@ List all conversation threads for a specific user | Parameter | Type | Description | | --------- | ---- | ----------- | | `threads` | array | Array of thread objects for this user | -| `userId` | string | The user ID | +| `totalCount` | number | Total number of threads returned | diff --git a/apps/sim/blocks/blocks/spotify.ts b/apps/sim/blocks/blocks/spotify.ts new file mode 100644 index 000000000..3987dc1b2 --- /dev/null +++ b/apps/sim/blocks/blocks/spotify.ts @@ -0,0 +1,1355 @@ +import { SpotifyIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { ToolResponse } from '@/tools/types' + +export const SpotifyBlock: BlockConfig = { + type: 'spotify', + name: 'Spotify', + description: 'Search music, manage playlists, control playback, and access your library', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate Spotify into your workflow. Search for tracks, albums, artists, and playlists. Manage playlists, access your library, control playback, browse podcasts and audiobooks.', + docsLink: 'https://docs.sim.ai/tools/spotify', + category: 'tools', + bgColor: '#000000', + icon: SpotifyIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Search & Discovery + { label: 'Search', id: 'spotify_search', group: 'Search & Discovery' }, + // Tracks + { label: 'Get Track', id: 'spotify_get_track', group: 'Tracks' }, + { label: 'Get Multiple Tracks', id: 'spotify_get_tracks', group: 'Tracks' }, + // Albums + { label: 'Get Album', id: 'spotify_get_album', group: 'Albums' }, + { label: 'Get Multiple Albums', id: 'spotify_get_albums', group: 'Albums' }, + { label: 'Get Album Tracks', id: 'spotify_get_album_tracks', group: 'Albums' }, + { label: 'Get Saved Albums', id: 'spotify_get_saved_albums', group: 'Albums' }, + { label: 'Save Albums', id: 'spotify_save_albums', group: 'Albums' }, + { label: 'Remove Saved Albums', id: 'spotify_remove_saved_albums', group: 'Albums' }, + { label: 'Check Saved Albums', id: 'spotify_check_saved_albums', group: 'Albums' }, + // Artists + { label: 'Get Artist', id: 'spotify_get_artist', group: 'Artists' }, + { label: 'Get Multiple Artists', id: 'spotify_get_artists', group: 'Artists' }, + { label: 'Get Artist Albums', id: 'spotify_get_artist_albums', group: 'Artists' }, + { label: 'Get Artist Top Tracks', id: 'spotify_get_artist_top_tracks', group: 'Artists' }, + { label: 'Follow Artists', id: 'spotify_follow_artists', group: 'Artists' }, + { label: 'Unfollow Artists', id: 'spotify_unfollow_artists', group: 'Artists' }, + { label: 'Get Followed Artists', id: 'spotify_get_followed_artists', group: 'Artists' }, + { label: 'Check Following', id: 'spotify_check_following', group: 'Artists' }, + // Shows (Podcasts) + { label: 'Get Show', id: 'spotify_get_show', group: 'Podcasts' }, + { label: 'Get Multiple Shows', id: 'spotify_get_shows', group: 'Podcasts' }, + { label: 'Get Show Episodes', id: 'spotify_get_show_episodes', group: 'Podcasts' }, + { label: 'Get Saved Shows', id: 'spotify_get_saved_shows', group: 'Podcasts' }, + { label: 'Save Shows', id: 'spotify_save_shows', group: 'Podcasts' }, + { label: 'Remove Saved Shows', id: 'spotify_remove_saved_shows', group: 'Podcasts' }, + { label: 'Check Saved Shows', id: 'spotify_check_saved_shows', group: 'Podcasts' }, + // Episodes + { label: 'Get Episode', id: 'spotify_get_episode', group: 'Episodes' }, + { label: 'Get Multiple Episodes', id: 'spotify_get_episodes', group: 'Episodes' }, + { label: 'Get Saved Episodes', id: 'spotify_get_saved_episodes', group: 'Episodes' }, + { label: 'Save Episodes', id: 'spotify_save_episodes', group: 'Episodes' }, + { label: 'Remove Saved Episodes', id: 'spotify_remove_saved_episodes', group: 'Episodes' }, + { label: 'Check Saved Episodes', id: 'spotify_check_saved_episodes', group: 'Episodes' }, + // Audiobooks + { label: 'Get Audiobook', id: 'spotify_get_audiobook', group: 'Audiobooks' }, + { label: 'Get Multiple Audiobooks', id: 'spotify_get_audiobooks', group: 'Audiobooks' }, + { + label: 'Get Audiobook Chapters', + id: 'spotify_get_audiobook_chapters', + group: 'Audiobooks', + }, + { label: 'Get Saved Audiobooks', id: 'spotify_get_saved_audiobooks', group: 'Audiobooks' }, + { label: 'Save Audiobooks', id: 'spotify_save_audiobooks', group: 'Audiobooks' }, + { + label: 'Remove Saved Audiobooks', + id: 'spotify_remove_saved_audiobooks', + group: 'Audiobooks', + }, + { + label: 'Check Saved Audiobooks', + id: 'spotify_check_saved_audiobooks', + group: 'Audiobooks', + }, + // Playlists + { label: 'Get Playlist', id: 'spotify_get_playlist', group: 'Playlists' }, + { label: 'Get Playlist Tracks', id: 'spotify_get_playlist_tracks', group: 'Playlists' }, + { label: 'Get Playlist Cover', id: 'spotify_get_playlist_cover', group: 'Playlists' }, + { label: 'Get My Playlists', id: 'spotify_get_user_playlists', group: 'Playlists' }, + { label: 'Create Playlist', id: 'spotify_create_playlist', group: 'Playlists' }, + { label: 'Update Playlist', id: 'spotify_update_playlist', group: 'Playlists' }, + { label: 'Add Playlist Cover', id: 'spotify_add_playlist_cover', group: 'Playlists' }, + { + label: 'Add Tracks to Playlist', + id: 'spotify_add_tracks_to_playlist', + group: 'Playlists', + }, + { + label: 'Remove Tracks from Playlist', + id: 'spotify_remove_tracks_from_playlist', + group: 'Playlists', + }, + { + label: 'Reorder Playlist Items', + id: 'spotify_reorder_playlist_items', + group: 'Playlists', + }, + { + label: 'Replace Playlist Items', + id: 'spotify_replace_playlist_items', + group: 'Playlists', + }, + { label: 'Follow Playlist', id: 'spotify_follow_playlist', group: 'Playlists' }, + { label: 'Unfollow Playlist', id: 'spotify_unfollow_playlist', group: 'Playlists' }, + { + label: 'Check Playlist Followers', + id: 'spotify_check_playlist_followers', + group: 'Playlists', + }, + // User Profile & Library + { label: 'Get My Profile', id: 'spotify_get_current_user', group: 'User & Library' }, + { label: 'Get User Profile', id: 'spotify_get_user_profile', group: 'User & Library' }, + { label: 'Get My Top Tracks', id: 'spotify_get_top_tracks', group: 'User & Library' }, + { label: 'Get My Top Artists', id: 'spotify_get_top_artists', group: 'User & Library' }, + { label: 'Get Saved Tracks', id: 'spotify_get_saved_tracks', group: 'User & Library' }, + { label: 'Save Tracks', id: 'spotify_save_tracks', group: 'User & Library' }, + { + label: 'Remove Saved Tracks', + id: 'spotify_remove_saved_tracks', + group: 'User & Library', + }, + { label: 'Check Saved Tracks', id: 'spotify_check_saved_tracks', group: 'User & Library' }, + { + label: 'Get Recently Played', + id: 'spotify_get_recently_played', + group: 'User & Library', + }, + // Browse + { label: 'Get New Releases', id: 'spotify_get_new_releases', group: 'Browse' }, + { label: 'Get Categories', id: 'spotify_get_categories', group: 'Browse' }, + { label: 'Get Available Markets', id: 'spotify_get_markets', group: 'Browse' }, + // Player Controls + { label: 'Get Playback State', id: 'spotify_get_playback_state', group: 'Player' }, + { label: 'Get Currently Playing', id: 'spotify_get_currently_playing', group: 'Player' }, + { label: 'Get Devices', id: 'spotify_get_devices', group: 'Player' }, + { label: 'Get Queue', id: 'spotify_get_queue', group: 'Player' }, + { label: 'Play', id: 'spotify_play', group: 'Player' }, + { label: 'Pause', id: 'spotify_pause', group: 'Player' }, + { label: 'Skip to Next', id: 'spotify_skip_next', group: 'Player' }, + { label: 'Skip to Previous', id: 'spotify_skip_previous', group: 'Player' }, + { label: 'Seek', id: 'spotify_seek', group: 'Player' }, + { label: 'Add to Queue', id: 'spotify_add_to_queue', group: 'Player' }, + { label: 'Set Volume', id: 'spotify_set_volume', group: 'Player' }, + { label: 'Set Repeat', id: 'spotify_set_repeat', group: 'Player' }, + { label: 'Set Shuffle', id: 'spotify_set_shuffle', group: 'Player' }, + { label: 'Transfer Playback', id: 'spotify_transfer_playback', group: 'Player' }, + ], + value: () => 'spotify_search', + }, + + // === SEARCH === + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'e.g., "Bohemian Rhapsody", "artist:Queen"', + required: true, + condition: { field: 'operation', value: 'spotify_search' }, + }, + { + id: 'type', + title: 'Search Type', + type: 'dropdown', + options: [ + { label: 'Tracks', id: 'track' }, + { label: 'Albums', id: 'album' }, + { label: 'Artists', id: 'artist' }, + { label: 'Playlists', id: 'playlist' }, + { label: 'Shows', id: 'show' }, + { label: 'Episodes', id: 'episode' }, + { label: 'Audiobooks', id: 'audiobook' }, + { label: 'All', id: 'track,album,artist,playlist' }, + ], + value: () => 'track', + condition: { field: 'operation', value: 'spotify_search' }, + }, + + // === TRACK IDs === + { + id: 'trackId', + title: 'Track ID', + type: 'short-input', + placeholder: 'Spotify track ID', + required: true, + condition: { field: 'operation', value: 'spotify_get_track' }, + }, + { + id: 'trackIds', + title: 'Track IDs', + type: 'short-input', + placeholder: 'Comma-separated track IDs', + required: true, + condition: { + field: 'operation', + value: [ + 'spotify_get_tracks', + 'spotify_save_tracks', + 'spotify_remove_saved_tracks', + 'spotify_check_saved_tracks', + ], + }, + }, + + // === ALBUM ID === + { + id: 'albumId', + title: 'Album ID', + type: 'short-input', + placeholder: 'Spotify album ID', + required: true, + condition: { field: 'operation', value: ['spotify_get_album', 'spotify_get_album_tracks'] }, + }, + { + id: 'albumIds', + title: 'Album IDs', + type: 'short-input', + placeholder: 'Comma-separated album IDs', + required: true, + condition: { + field: 'operation', + value: [ + 'spotify_get_albums', + 'spotify_save_albums', + 'spotify_remove_saved_albums', + 'spotify_check_saved_albums', + ], + }, + }, + + // === ARTIST ID === + { + id: 'artistId', + title: 'Artist ID', + type: 'short-input', + placeholder: 'Spotify artist ID', + required: true, + condition: { + field: 'operation', + value: ['spotify_get_artist', 'spotify_get_artist_albums', 'spotify_get_artist_top_tracks'], + }, + }, + { + id: 'artistIds', + title: 'Artist IDs', + type: 'short-input', + placeholder: 'Comma-separated artist IDs', + required: true, + condition: { + field: 'operation', + value: ['spotify_get_artists', 'spotify_follow_artists', 'spotify_unfollow_artists'], + }, + }, + + // === SHOW IDs === + { + id: 'showId', + title: 'Show ID', + type: 'short-input', + placeholder: 'Spotify show/podcast ID', + required: true, + condition: { field: 'operation', value: ['spotify_get_show', 'spotify_get_show_episodes'] }, + }, + { + id: 'showIds', + title: 'Show IDs', + type: 'short-input', + placeholder: 'Comma-separated show IDs', + required: true, + condition: { + field: 'operation', + value: [ + 'spotify_get_shows', + 'spotify_save_shows', + 'spotify_remove_saved_shows', + 'spotify_check_saved_shows', + ], + }, + }, + + // === EPISODE IDs === + { + id: 'episodeId', + title: 'Episode ID', + type: 'short-input', + placeholder: 'Spotify episode ID', + required: true, + condition: { field: 'operation', value: 'spotify_get_episode' }, + }, + { + id: 'episodeIds', + title: 'Episode IDs', + type: 'short-input', + placeholder: 'Comma-separated episode IDs', + required: true, + condition: { + field: 'operation', + value: [ + 'spotify_get_episodes', + 'spotify_save_episodes', + 'spotify_remove_saved_episodes', + 'spotify_check_saved_episodes', + ], + }, + }, + + // === AUDIOBOOK IDs === + { + id: 'audiobookId', + title: 'Audiobook ID', + type: 'short-input', + placeholder: 'Spotify audiobook ID', + required: true, + condition: { + field: 'operation', + value: ['spotify_get_audiobook', 'spotify_get_audiobook_chapters'], + }, + }, + { + id: 'audiobookIds', + title: 'Audiobook IDs', + type: 'short-input', + placeholder: 'Comma-separated audiobook IDs', + required: true, + condition: { + field: 'operation', + value: [ + 'spotify_get_audiobooks', + 'spotify_save_audiobooks', + 'spotify_remove_saved_audiobooks', + 'spotify_check_saved_audiobooks', + ], + }, + }, + + // === CHECK FOLLOWING === + { + id: 'followType', + title: 'Type', + type: 'dropdown', + options: [ + { label: 'Artist', id: 'artist' }, + { label: 'User', id: 'user' }, + ], + value: () => 'artist', + condition: { field: 'operation', value: 'spotify_check_following' }, + }, + { + id: 'ids', + title: 'IDs to Check', + type: 'short-input', + placeholder: 'Comma-separated artist or user IDs', + required: true, + condition: { field: 'operation', value: 'spotify_check_following' }, + }, + + // === USER ID === + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Spotify user ID', + required: true, + condition: { field: 'operation', value: 'spotify_get_user_profile' }, + }, + + // === PLAYLIST OPERATIONS === + { + id: 'playlistId', + title: 'Playlist ID', + type: 'short-input', + placeholder: 'Spotify playlist ID', + required: true, + condition: { + field: 'operation', + value: [ + 'spotify_get_playlist', + 'spotify_get_playlist_tracks', + 'spotify_get_playlist_cover', + 'spotify_update_playlist', + 'spotify_add_playlist_cover', + 'spotify_add_tracks_to_playlist', + 'spotify_remove_tracks_from_playlist', + 'spotify_reorder_playlist_items', + 'spotify_replace_playlist_items', + 'spotify_follow_playlist', + 'spotify_unfollow_playlist', + 'spotify_check_playlist_followers', + ], + }, + }, + { + id: 'name', + title: 'Playlist Name', + type: 'short-input', + placeholder: 'My Awesome Playlist', + required: true, + condition: { field: 'operation', value: 'spotify_create_playlist' }, + }, + { + id: 'newName', + title: 'New Name', + type: 'short-input', + placeholder: 'New playlist name (optional)', + condition: { field: 'operation', value: 'spotify_update_playlist' }, + }, + { + id: 'description', + title: 'Playlist Description', + type: 'long-input', + placeholder: 'Optional description for the playlist', + condition: { + field: 'operation', + value: ['spotify_create_playlist', 'spotify_update_playlist'], + }, + }, + { + id: 'public', + title: 'Public', + type: 'switch', + defaultValue: true, + condition: { + field: 'operation', + value: ['spotify_create_playlist', 'spotify_update_playlist', 'spotify_follow_playlist'], + }, + }, + + // === CHECK PLAYLIST FOLLOWERS === + { + id: 'userIds', + title: 'User IDs', + type: 'short-input', + placeholder: 'Comma-separated user IDs to check (max 5)', + required: true, + condition: { field: 'operation', value: 'spotify_check_playlist_followers' }, + }, + + // === PLAYLIST COVER === + { + id: 'imageBase64', + title: 'Image (Base64)', + type: 'long-input', + placeholder: 'Base64-encoded JPEG image (max 256KB)', + required: true, + condition: { field: 'operation', value: 'spotify_add_playlist_cover' }, + }, + + // === REORDER PLAYLIST === + { + id: 'range_start', + title: 'Range Start', + type: 'short-input', + placeholder: 'Start index of items to move', + required: true, + condition: { field: 'operation', value: 'spotify_reorder_playlist_items' }, + }, + { + id: 'insert_before', + title: 'Insert Before', + type: 'short-input', + placeholder: 'Index to insert before', + required: true, + condition: { field: 'operation', value: 'spotify_reorder_playlist_items' }, + }, + { + id: 'range_length', + title: 'Range Length', + type: 'short-input', + placeholder: 'Number of items to move (default: 1)', + condition: { field: 'operation', value: 'spotify_reorder_playlist_items' }, + }, + + // === ADD/REMOVE/REPLACE TRACKS FROM PLAYLIST === + { + id: 'uris', + title: 'Track URIs', + type: 'short-input', + placeholder: 'spotify:track:xxx,spotify:track:yyy', + required: true, + condition: { + field: 'operation', + value: [ + 'spotify_add_tracks_to_playlist', + 'spotify_remove_tracks_from_playlist', + 'spotify_replace_playlist_items', + ], + }, + }, + + // === COUNTRY/LOCALE === + { + id: 'country', + title: 'Country', + type: 'short-input', + placeholder: 'ISO country code (e.g., US, GB)', + condition: { + field: 'operation', + value: ['spotify_get_new_releases', 'spotify_get_categories'], + }, + }, + + // === TOP ITEMS TIME RANGE === + { + id: 'time_range', + title: 'Time Range', + type: 'dropdown', + options: [ + { label: 'Last 4 Weeks', id: 'short_term' }, + { label: 'Last 6 Months', id: 'medium_term' }, + { label: 'All Time', id: 'long_term' }, + ], + value: () => 'medium_term', + condition: { + field: 'operation', + value: ['spotify_get_top_tracks', 'spotify_get_top_artists'], + }, + }, + + // === PLAYER CONTROLS === + { + id: 'device_id', + title: 'Device ID', + type: 'short-input', + placeholder: 'Optional - uses active device if not specified', + condition: { + field: 'operation', + value: [ + 'spotify_play', + 'spotify_pause', + 'spotify_skip_next', + 'spotify_skip_previous', + 'spotify_add_to_queue', + 'spotify_set_volume', + 'spotify_seek', + 'spotify_set_repeat', + 'spotify_set_shuffle', + ], + }, + }, + { + id: 'context_uri', + title: 'Context URI', + type: 'short-input', + placeholder: 'spotify:album:xxx or spotify:playlist:yyy', + condition: { field: 'operation', value: 'spotify_play' }, + }, + { + id: 'playUris', + title: 'Track URIs', + type: 'short-input', + placeholder: 'spotify:track:xxx (comma-separated for multiple)', + condition: { field: 'operation', value: 'spotify_play' }, + }, + { + id: 'uri', + title: 'Track URI', + type: 'short-input', + placeholder: 'spotify:track:xxx', + required: true, + condition: { field: 'operation', value: 'spotify_add_to_queue' }, + }, + { + id: 'volume_percent', + title: 'Volume', + type: 'slider', + min: 0, + max: 100, + step: 1, + integer: true, + condition: { field: 'operation', value: 'spotify_set_volume' }, + }, + + // === SEEK === + { + id: 'position_ms', + title: 'Position (ms)', + type: 'short-input', + placeholder: 'Position in milliseconds', + required: true, + condition: { field: 'operation', value: 'spotify_seek' }, + }, + + // === REPEAT === + { + id: 'state', + title: 'Repeat Mode', + type: 'dropdown', + options: [ + { label: 'Off', id: 'off' }, + { label: 'Track', id: 'track' }, + { label: 'Context (Album/Playlist)', id: 'context' }, + ], + value: () => 'off', + condition: { field: 'operation', value: 'spotify_set_repeat' }, + }, + + // === SHUFFLE === + { + id: 'shuffle_state', + title: 'Shuffle', + type: 'switch', + defaultValue: false, + condition: { field: 'operation', value: 'spotify_set_shuffle' }, + }, + + // === TRANSFER PLAYBACK === + { + id: 'target_device_id', + title: 'Target Device ID', + type: 'short-input', + placeholder: 'Device ID to transfer to', + required: true, + condition: { field: 'operation', value: 'spotify_transfer_playback' }, + }, + + // === COMMON: LIMIT === + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Number of results (1-50, default: 20)', + condition: { + field: 'operation', + value: [ + 'spotify_search', + 'spotify_get_album_tracks', + 'spotify_get_saved_albums', + 'spotify_get_artist_albums', + 'spotify_get_playlist_tracks', + 'spotify_get_user_playlists', + 'spotify_get_top_tracks', + 'spotify_get_top_artists', + 'spotify_get_saved_tracks', + 'spotify_get_recently_played', + 'spotify_get_new_releases', + 'spotify_get_categories', + 'spotify_get_followed_artists', + 'spotify_get_show_episodes', + 'spotify_get_saved_shows', + 'spotify_get_saved_episodes', + 'spotify_get_audiobook_chapters', + 'spotify_get_saved_audiobooks', + ], + }, + }, + + // === OAUTH CREDENTIAL === + { + id: 'credential', + title: 'Spotify Account', + type: 'oauth-input', + serviceId: 'spotify', + required: true, + }, + ], + tools: { + access: [ + 'spotify_search', + 'spotify_get_track', + 'spotify_get_tracks', + 'spotify_get_album', + 'spotify_get_albums', + 'spotify_get_album_tracks', + 'spotify_get_saved_albums', + 'spotify_save_albums', + 'spotify_remove_saved_albums', + 'spotify_check_saved_albums', + 'spotify_get_artist', + 'spotify_get_artists', + 'spotify_get_artist_albums', + 'spotify_get_artist_top_tracks', + 'spotify_follow_artists', + 'spotify_unfollow_artists', + 'spotify_get_followed_artists', + 'spotify_check_following', + 'spotify_get_show', + 'spotify_get_shows', + 'spotify_get_show_episodes', + 'spotify_get_saved_shows', + 'spotify_save_shows', + 'spotify_remove_saved_shows', + 'spotify_check_saved_shows', + 'spotify_get_episode', + 'spotify_get_episodes', + 'spotify_get_saved_episodes', + 'spotify_save_episodes', + 'spotify_remove_saved_episodes', + 'spotify_check_saved_episodes', + 'spotify_get_audiobook', + 'spotify_get_audiobooks', + 'spotify_get_audiobook_chapters', + 'spotify_get_saved_audiobooks', + 'spotify_save_audiobooks', + 'spotify_remove_saved_audiobooks', + 'spotify_check_saved_audiobooks', + 'spotify_get_playlist', + 'spotify_get_playlist_tracks', + 'spotify_get_playlist_cover', + 'spotify_get_user_playlists', + 'spotify_create_playlist', + 'spotify_update_playlist', + 'spotify_add_playlist_cover', + 'spotify_add_tracks_to_playlist', + 'spotify_remove_tracks_from_playlist', + 'spotify_reorder_playlist_items', + 'spotify_replace_playlist_items', + 'spotify_follow_playlist', + 'spotify_unfollow_playlist', + 'spotify_check_playlist_followers', + 'spotify_get_current_user', + 'spotify_get_user_profile', + 'spotify_get_top_tracks', + 'spotify_get_top_artists', + 'spotify_get_saved_tracks', + 'spotify_save_tracks', + 'spotify_remove_saved_tracks', + 'spotify_check_saved_tracks', + 'spotify_get_recently_played', + 'spotify_get_new_releases', + 'spotify_get_categories', + 'spotify_get_markets', + 'spotify_get_playback_state', + 'spotify_get_currently_playing', + 'spotify_get_devices', + 'spotify_get_queue', + 'spotify_play', + 'spotify_pause', + 'spotify_skip_next', + 'spotify_skip_previous', + 'spotify_seek', + 'spotify_add_to_queue', + 'spotify_set_volume', + 'spotify_set_repeat', + 'spotify_set_shuffle', + 'spotify_transfer_playback', + ], + config: { + tool: (params) => { + // Convert numeric parameters + if (params.limit) { + params.limit = Number(params.limit) + } + if (params.volume_percent) { + params.volume_percent = Number(params.volume_percent) + } + if (params.range_start) { + params.range_start = Number(params.range_start) + } + if (params.insert_before) { + params.insert_before = Number(params.insert_before) + } + if (params.range_length) { + params.range_length = Number(params.range_length) + } + if (params.position_ms) { + params.position_ms = Number(params.position_ms) + } + // Map followType to type for check_following + if (params.followType) { + params.type = params.followType + } + // Map newName to name for update_playlist + if (params.newName) { + params.name = params.newName + } + // Map playUris to uris for play + if (params.playUris) { + params.uris = params.playUris + } + return params.operation || 'spotify_search' + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Spotify OAuth credential' }, + // Search + query: { type: 'string', description: 'Search query' }, + type: { type: 'string', description: 'Search type' }, + // IDs + trackId: { type: 'string', description: 'Spotify track ID' }, + trackIds: { type: 'string', description: 'Comma-separated track IDs' }, + albumId: { type: 'string', description: 'Spotify album ID' }, + albumIds: { type: 'string', description: 'Comma-separated album IDs' }, + artistId: { type: 'string', description: 'Spotify artist ID' }, + artistIds: { type: 'string', description: 'Comma-separated artist IDs' }, + showId: { type: 'string', description: 'Spotify show ID' }, + showIds: { type: 'string', description: 'Comma-separated show IDs' }, + episodeId: { type: 'string', description: 'Spotify episode ID' }, + episodeIds: { type: 'string', description: 'Comma-separated episode IDs' }, + audiobookId: { type: 'string', description: 'Spotify audiobook ID' }, + audiobookIds: { type: 'string', description: 'Comma-separated audiobook IDs' }, + playlistId: { type: 'string', description: 'Spotify playlist ID' }, + userId: { type: 'string', description: 'Spotify user ID' }, + userIds: { type: 'string', description: 'Comma-separated user IDs' }, + ids: { type: 'string', description: 'Comma-separated IDs' }, + followType: { type: 'string', description: 'Type to check (artist or user)' }, + // Playlist + name: { type: 'string', description: 'Playlist name' }, + newName: { type: 'string', description: 'New playlist name' }, + description: { type: 'string', description: 'Playlist description' }, + public: { type: 'boolean', description: 'Whether playlist is public' }, + imageBase64: { type: 'string', description: 'Base64-encoded JPEG image' }, + range_start: { type: 'number', description: 'Start index for reorder' }, + insert_before: { type: 'number', description: 'Insert before index' }, + range_length: { type: 'number', description: 'Number of items to move' }, + // Track URIs + uris: { type: 'string', description: 'Comma-separated Spotify URIs' }, + playUris: { type: 'string', description: 'Track URIs to play' }, + uri: { type: 'string', description: 'Spotify URI' }, + // Time range + time_range: { type: 'string', description: 'Time range for top items' }, + // Browse + country: { type: 'string', description: 'ISO country code' }, + // Player + device_id: { type: 'string', description: 'Device ID for playback' }, + context_uri: { type: 'string', description: 'Context URI (album, playlist, artist)' }, + volume_percent: { type: 'number', description: 'Volume level (0-100)' }, + position_ms: { type: 'number', description: 'Position in milliseconds' }, + state: { type: 'string', description: 'Repeat mode (off, track, context)' }, + shuffle_state: { type: 'boolean', description: 'Shuffle on/off' }, + target_device_id: { type: 'string', description: 'Target device ID for transfer' }, + // Common + limit: { type: 'number', description: 'Maximum number of results' }, + }, + outputs: { + // === SEARCH OUTPUTS === + tracks: { + type: 'json', + description: 'List of tracks', + condition: { + field: 'operation', + value: [ + 'spotify_search', + 'spotify_get_tracks', + 'spotify_get_album_tracks', + 'spotify_get_playlist_tracks', + 'spotify_get_artist_top_tracks', + 'spotify_get_saved_tracks', + 'spotify_get_top_tracks', + ], + }, + }, + artists: { + type: 'json', + description: 'List of artists', + condition: { + field: 'operation', + value: [ + 'spotify_search', + 'spotify_get_artists', + 'spotify_get_top_artists', + 'spotify_get_followed_artists', + ], + }, + }, + albums: { + type: 'json', + description: 'List of albums', + condition: { + field: 'operation', + value: [ + 'spotify_search', + 'spotify_get_albums', + 'spotify_get_artist_albums', + 'spotify_get_saved_albums', + 'spotify_get_new_releases', + ], + }, + }, + playlists: { + type: 'json', + description: 'List of playlists', + condition: { field: 'operation', value: ['spotify_search', 'spotify_get_user_playlists'] }, + }, + + // === SINGLE ITEM OUTPUTS === + id: { + type: 'string', + description: 'Spotify ID', + condition: { + field: 'operation', + value: [ + 'spotify_get_track', + 'spotify_get_album', + 'spotify_get_artist', + 'spotify_get_playlist', + 'spotify_get_show', + 'spotify_get_episode', + 'spotify_get_audiobook', + 'spotify_get_current_user', + 'spotify_get_user_profile', + 'spotify_create_playlist', + ], + }, + }, + name: { + type: 'string', + description: 'Name', + condition: { + field: 'operation', + value: [ + 'spotify_get_track', + 'spotify_get_album', + 'spotify_get_artist', + 'spotify_get_playlist', + 'spotify_get_show', + 'spotify_get_episode', + 'spotify_get_audiobook', + 'spotify_create_playlist', + ], + }, + }, + uri: { + type: 'string', + description: 'Spotify URI', + condition: { field: 'operation', value: 'spotify_get_track' }, + }, + external_url: { + type: 'string', + description: 'Spotify URL', + condition: { + field: 'operation', + value: [ + 'spotify_get_track', + 'spotify_get_album', + 'spotify_get_artist', + 'spotify_get_playlist', + 'spotify_get_show', + 'spotify_get_episode', + 'spotify_get_audiobook', + 'spotify_get_current_user', + 'spotify_get_user_profile', + 'spotify_create_playlist', + ], + }, + }, + image_url: { + type: 'string', + description: 'Cover/profile image URL', + condition: { + field: 'operation', + value: [ + 'spotify_get_track', + 'spotify_get_album', + 'spotify_get_artist', + 'spotify_get_playlist', + 'spotify_get_show', + 'spotify_get_episode', + 'spotify_get_audiobook', + 'spotify_get_current_user', + 'spotify_get_user_profile', + 'spotify_get_playlist_cover', + ], + }, + }, + popularity: { + type: 'number', + description: 'Popularity score (0-100)', + condition: { + field: 'operation', + value: ['spotify_get_track', 'spotify_get_album', 'spotify_get_artist'], + }, + }, + + // === TRACK OUTPUTS === + album: { + type: 'json', + description: 'Album information', + condition: { field: 'operation', value: 'spotify_get_track' }, + }, + duration_ms: { + type: 'number', + description: 'Duration in milliseconds', + condition: { field: 'operation', value: ['spotify_get_track', 'spotify_get_episode'] }, + }, + explicit: { + type: 'boolean', + description: 'Contains explicit content', + condition: { + field: 'operation', + value: ['spotify_get_track', 'spotify_get_show', 'spotify_get_episode'], + }, + }, + preview_url: { + type: 'string', + description: 'URL to 30-second preview', + condition: { field: 'operation', value: 'spotify_get_track' }, + }, + + // === ALBUM OUTPUTS === + album_type: { + type: 'string', + description: 'Album type (album, single, compilation)', + condition: { field: 'operation', value: 'spotify_get_album' }, + }, + release_date: { + type: 'string', + description: 'Release date', + condition: { field: 'operation', value: ['spotify_get_album', 'spotify_get_episode'] }, + }, + label: { + type: 'string', + description: 'Record label', + condition: { field: 'operation', value: 'spotify_get_album' }, + }, + total_tracks: { + type: 'number', + description: 'Total tracks', + condition: { field: 'operation', value: ['spotify_get_album', 'spotify_get_playlist'] }, + }, + genres: { + type: 'json', + description: 'List of genres', + condition: { field: 'operation', value: ['spotify_get_album', 'spotify_get_artist'] }, + }, + + // === ARTIST OUTPUTS === + followers: { + type: 'number', + description: 'Number of followers', + condition: { + field: 'operation', + value: ['spotify_get_artist', 'spotify_get_current_user', 'spotify_get_user_profile'], + }, + }, + + // === PLAYLIST OUTPUTS === + description: { + type: 'string', + description: 'Description', + condition: { + field: 'operation', + value: [ + 'spotify_get_playlist', + 'spotify_get_show', + 'spotify_get_episode', + 'spotify_get_audiobook', + 'spotify_create_playlist', + ], + }, + }, + owner: { + type: 'json', + description: 'Playlist owner information', + condition: { field: 'operation', value: ['spotify_get_playlist', 'spotify_create_playlist'] }, + }, + public: { + type: 'boolean', + description: 'Whether playlist is public', + condition: { field: 'operation', value: ['spotify_get_playlist', 'spotify_create_playlist'] }, + }, + collaborative: { + type: 'boolean', + description: 'Whether playlist is collaborative', + condition: { field: 'operation', value: ['spotify_get_playlist', 'spotify_create_playlist'] }, + }, + snapshot_id: { + type: 'string', + description: 'Playlist version snapshot ID', + condition: { + field: 'operation', + value: [ + 'spotify_get_playlist', + 'spotify_create_playlist', + 'spotify_add_tracks_to_playlist', + 'spotify_remove_tracks_from_playlist', + 'spotify_reorder_playlist_items', + 'spotify_replace_playlist_items', + ], + }, + }, + + // === SHOW/PODCAST OUTPUTS === + publisher: { + type: 'string', + description: 'Publisher name', + condition: { field: 'operation', value: ['spotify_get_show', 'spotify_get_audiobook'] }, + }, + total_episodes: { + type: 'number', + description: 'Total episodes in show', + condition: { field: 'operation', value: 'spotify_get_show' }, + }, + shows: { + type: 'json', + description: 'List of shows/podcasts', + condition: { field: 'operation', value: ['spotify_get_shows', 'spotify_get_saved_shows'] }, + }, + languages: { + type: 'json', + description: 'List of languages', + condition: { field: 'operation', value: ['spotify_get_show', 'spotify_get_audiobook'] }, + }, + + // === EPISODE OUTPUTS === + show: { + type: 'json', + description: 'Parent show information', + condition: { field: 'operation', value: 'spotify_get_episode' }, + }, + episodes: { + type: 'json', + description: 'List of episodes', + condition: { + field: 'operation', + value: ['spotify_get_episodes', 'spotify_get_show_episodes', 'spotify_get_saved_episodes'], + }, + }, + + // === AUDIOBOOK OUTPUTS === + authors: { + type: 'json', + description: 'List of authors', + condition: { field: 'operation', value: 'spotify_get_audiobook' }, + }, + narrators: { + type: 'json', + description: 'List of narrators', + condition: { field: 'operation', value: 'spotify_get_audiobook' }, + }, + total_chapters: { + type: 'number', + description: 'Total chapters', + condition: { field: 'operation', value: 'spotify_get_audiobook' }, + }, + audiobooks: { + type: 'json', + description: 'List of audiobooks', + condition: { + field: 'operation', + value: ['spotify_get_audiobooks', 'spotify_get_saved_audiobooks'], + }, + }, + chapters: { + type: 'json', + description: 'List of chapters', + condition: { field: 'operation', value: 'spotify_get_audiobook_chapters' }, + }, + + // === USER PROFILE OUTPUTS === + display_name: { + type: 'string', + description: 'User display name', + condition: { + field: 'operation', + value: ['spotify_get_current_user', 'spotify_get_user_profile'], + }, + }, + email: { + type: 'string', + description: 'User email address', + condition: { field: 'operation', value: 'spotify_get_current_user' }, + }, + country: { + type: 'string', + description: 'User country code', + condition: { field: 'operation', value: 'spotify_get_current_user' }, + }, + product: { + type: 'string', + description: 'Subscription level (free, premium)', + condition: { field: 'operation', value: 'spotify_get_current_user' }, + }, + + // === PLAYER STATE OUTPUTS === + is_playing: { + type: 'boolean', + description: 'Whether playback is active', + condition: { + field: 'operation', + value: ['spotify_get_playback_state', 'spotify_get_currently_playing'], + }, + }, + device: { + type: 'json', + description: 'Active device information', + condition: { field: 'operation', value: 'spotify_get_playback_state' }, + }, + devices: { + type: 'json', + description: 'Available playback devices', + condition: { field: 'operation', value: 'spotify_get_devices' }, + }, + progress_ms: { + type: 'number', + description: 'Current playback position in ms', + condition: { + field: 'operation', + value: ['spotify_get_playback_state', 'spotify_get_currently_playing'], + }, + }, + currently_playing_type: { + type: 'string', + description: 'Type of content (track, episode, ad)', + condition: { field: 'operation', value: 'spotify_get_playback_state' }, + }, + shuffle_state: { + type: 'boolean', + description: 'Whether shuffle is enabled', + condition: { field: 'operation', value: 'spotify_get_playback_state' }, + }, + repeat_state: { + type: 'string', + description: 'Repeat mode (off, track, context)', + condition: { field: 'operation', value: 'spotify_get_playback_state' }, + }, + track: { + type: 'json', + description: 'Currently playing track', + condition: { + field: 'operation', + value: ['spotify_get_playback_state', 'spotify_get_currently_playing'], + }, + }, + currently_playing: { + type: 'json', + description: 'Currently playing item', + condition: { field: 'operation', value: 'spotify_get_queue' }, + }, + queue: { + type: 'json', + description: 'Upcoming tracks in queue', + condition: { field: 'operation', value: 'spotify_get_queue' }, + }, + + // === RECENTLY PLAYED OUTPUTS === + items: { + type: 'json', + description: 'List of recently played items', + condition: { field: 'operation', value: 'spotify_get_recently_played' }, + }, + + // === BROWSE OUTPUTS === + categories: { + type: 'json', + description: 'List of browse categories', + condition: { field: 'operation', value: 'spotify_get_categories' }, + }, + markets: { + type: 'json', + description: 'List of available market codes', + condition: { field: 'operation', value: 'spotify_get_markets' }, + }, + + // === CHECK SAVED OUTPUTS === + results: { + type: 'json', + description: 'Check operation results (id and saved boolean)', + condition: { + field: 'operation', + value: [ + 'spotify_check_saved_tracks', + 'spotify_check_saved_albums', + 'spotify_check_saved_shows', + 'spotify_check_saved_episodes', + 'spotify_check_saved_audiobooks', + 'spotify_check_following', + 'spotify_check_playlist_followers', + ], + }, + }, + all_saved: { + type: 'boolean', + description: 'Whether all tracks are saved', + condition: { field: 'operation', value: 'spotify_check_saved_tracks' }, + }, + none_saved: { + type: 'boolean', + description: 'Whether no tracks are saved', + condition: { field: 'operation', value: 'spotify_check_saved_tracks' }, + }, + + // === PAGINATION OUTPUTS === + total: { + type: 'number', + description: 'Total number of items', + condition: { + field: 'operation', + value: [ + 'spotify_get_album_tracks', + 'spotify_get_artist_albums', + 'spotify_get_playlist_tracks', + 'spotify_get_user_playlists', + 'spotify_get_saved_tracks', + 'spotify_get_saved_albums', + 'spotify_get_top_tracks', + 'spotify_get_top_artists', + 'spotify_get_new_releases', + 'spotify_get_categories', + ], + }, + }, + next: { + type: 'string', + description: 'URL for next page of results', + condition: { + field: 'operation', + value: [ + 'spotify_get_album_tracks', + 'spotify_get_artist_albums', + 'spotify_get_playlist_tracks', + 'spotify_get_user_playlists', + 'spotify_get_saved_tracks', + 'spotify_get_saved_albums', + 'spotify_get_top_tracks', + 'spotify_get_top_artists', + 'spotify_get_recently_played', + 'spotify_get_new_releases', + ], + }, + }, + + // === OPERATION RESULT OUTPUTS === + success: { + type: 'boolean', + description: 'Whether operation succeeded', + condition: { + field: 'operation', + value: [ + 'spotify_play', + 'spotify_pause', + 'spotify_skip_next', + 'spotify_skip_previous', + 'spotify_seek', + 'spotify_set_volume', + 'spotify_set_repeat', + 'spotify_set_shuffle', + 'spotify_transfer_playback', + 'spotify_add_to_queue', + 'spotify_save_tracks', + 'spotify_remove_saved_tracks', + 'spotify_save_albums', + 'spotify_remove_saved_albums', + 'spotify_save_shows', + 'spotify_remove_saved_shows', + 'spotify_save_episodes', + 'spotify_remove_saved_episodes', + 'spotify_save_audiobooks', + 'spotify_remove_saved_audiobooks', + 'spotify_follow_artists', + 'spotify_unfollow_artists', + 'spotify_follow_playlist', + 'spotify_unfollow_playlist', + 'spotify_update_playlist', + 'spotify_add_playlist_cover', + ], + }, + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index ef3d36c0f..ca1f30e84 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -101,6 +101,7 @@ import { SharepointBlock } from '@/blocks/blocks/sharepoint' import { ShopifyBlock } from '@/blocks/blocks/shopify' import { SlackBlock } from '@/blocks/blocks/slack' import { SmtpBlock } from '@/blocks/blocks/smtp' +import { SpotifyBlock } from '@/blocks/blocks/spotify' import { SSHBlock } from '@/blocks/blocks/ssh' import { StagehandBlock } from '@/blocks/blocks/stagehand' import { StartTriggerBlock } from '@/blocks/blocks/start_trigger' @@ -241,6 +242,7 @@ export const registry: Record = { sharepoint: SharepointBlock, shopify: ShopifyBlock, slack: SlackBlock, + spotify: SpotifyBlock, smtp: SmtpBlock, sftp: SftpBlock, ssh: SSHBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index e88000cab..f9e690a72 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4203,3 +4203,15 @@ export function RssIcon(props: SVGProps) { ) } + +export function SpotifyIcon(props: SVGProps) { + return ( + + + + + ) +} diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 901a37e8b..6123656f7 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -222,6 +222,7 @@ export const auth = betterAuth({ 'pipedrive', 'hubspot', 'linkedin', + 'spotify', // Common SSO provider patterns ...SSO_TRUSTED_PROVIDERS, @@ -1838,6 +1839,72 @@ export const auth = betterAuth({ }, }, + // Spotify provider + { + providerId: 'spotify', + clientId: env.SPOTIFY_CLIENT_ID as string, + clientSecret: env.SPOTIFY_CLIENT_SECRET as string, + authorizationUrl: 'https://accounts.spotify.com/authorize', + tokenUrl: 'https://accounts.spotify.com/api/token', + userInfoUrl: 'https://api.spotify.com/v1/me', + scopes: [ + 'user-read-private', + 'user-read-email', + 'user-library-read', + 'user-library-modify', + 'playlist-read-private', + 'playlist-read-collaborative', + 'playlist-modify-public', + 'playlist-modify-private', + 'user-read-playback-state', + 'user-modify-playback-state', + 'user-read-currently-playing', + 'user-read-recently-played', + 'user-top-read', + 'user-follow-read', + 'user-follow-modify', + 'user-read-playback-position', + 'ugc-image-upload', + ], + responseType: 'code', + authentication: 'basic', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/spotify`, + getUserInfo: async (tokens) => { + try { + logger.info('Fetching Spotify user profile') + + const response = await fetch('https://api.spotify.com/v1/me', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (!response.ok) { + logger.error('Failed to fetch Spotify user info', { + status: response.status, + statusText: response.statusText, + }) + throw new Error('Failed to fetch user info') + } + + const profile = await response.json() + + return { + id: profile.id, + name: profile.display_name || 'Spotify User', + email: profile.email || `${profile.id}@spotify.user`, + emailVerified: true, + image: profile.images?.[0]?.url || undefined, + createdAt: new Date(), + updatedAt: new Date(), + } + } catch (error) { + logger.error('Error in Spotify getUserInfo:', { error }) + return null + } + }, + }, + // WordPress.com provider { providerId: 'wordpress', diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index ac5ec6ffe..069aeff3e 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -230,6 +230,8 @@ export const env = createEnv({ ZOOM_CLIENT_SECRET: z.string().optional(), // Zoom OAuth client secret WORDPRESS_CLIENT_ID: z.string().optional(), // WordPress.com OAuth client ID WORDPRESS_CLIENT_SECRET: z.string().optional(), // WordPress.com OAuth client secret + SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID + SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret // E2B Remote Code Execution E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index 92b76ae36..f7c4f1277 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -175,9 +175,16 @@ export class LoggingSession { logger.debug(`[${this.requestId}] Completed logging for execution ${this.executionId}`) } } catch (error) { - if (this.requestId) { - logger.error(`[${this.requestId}] Failed to complete logging:`, error) - } + // Always log completion failures with full details - these should not be silent + logger.error(`Failed to complete logging for execution ${this.executionId}:`, { + requestId: this.requestId, + workflowId: this.workflowId, + executionId: this.executionId, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) + // Rethrow so safeComplete can decide what to do + throw error } } @@ -247,12 +254,21 @@ export class LoggingSession { } if (this.requestId) { - logger.debug(`[${this.requestId}] Completed logging for execution ${this.executionId}`) + logger.debug( + `[${this.requestId}] Completed error logging for execution ${this.executionId}` + ) } } catch (enhancedError) { - if (this.requestId) { - logger.error(`[${this.requestId}] Failed to complete logging:`, enhancedError) - } + // Always log completion failures with full details + logger.error(`Failed to complete error logging for execution ${this.executionId}:`, { + requestId: this.requestId, + workflowId: this.workflowId, + executionId: this.executionId, + error: enhancedError instanceof Error ? enhancedError.message : String(enhancedError), + stack: enhancedError instanceof Error ? enhancedError.stack : undefined, + }) + // Rethrow so safeCompleteWithError can decide what to do + throw enhancedError } } @@ -315,9 +331,10 @@ export class LoggingSession { try { await this.complete(params) } catch (error) { - if (this.requestId) { - logger.error(`[${this.requestId}] Logging completion failed:`, error) - } + // Error already logged in complete(), log a summary here + logger.warn( + `[${this.requestId || 'unknown'}] Logging completion failed for execution ${this.executionId} - execution data not persisted` + ) } } @@ -325,9 +342,10 @@ export class LoggingSession { try { await this.completeWithError(error) } catch (enhancedError) { - if (this.requestId) { - logger.error(`[${this.requestId}] Logging error completion failed:`, enhancedError) - } + // Error already logged in completeWithError(), log a summary here + logger.warn( + `[${this.requestId || 'unknown'}] Error logging completion failed for execution ${this.executionId} - execution data not persisted` + ) } } } diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 3e4181aad..7b4f53caa 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -31,6 +31,7 @@ import { SalesforceIcon, ShopifyIcon, SlackIcon, + SpotifyIcon, // SupabaseIcon, TrelloIcon, WealthboxIcon, @@ -70,6 +71,7 @@ export type OAuthProvider = | 'shopify' | 'zoom' | 'wordpress' + | 'spotify' | string export type OAuthService = @@ -111,6 +113,8 @@ export type OAuthService = | 'shopify' | 'zoom' | 'wordpress' + | 'spotify' + export interface OAuthProviderConfig { id: OAuthProvider name: string @@ -891,6 +895,41 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'wordpress', }, + spotify: { + id: 'spotify', + name: 'Spotify', + icon: (props) => SpotifyIcon(props), + services: { + spotify: { + id: 'spotify', + name: 'Spotify', + description: 'Search music, manage playlists, control playback, and access your library.', + providerId: 'spotify', + icon: (props) => SpotifyIcon(props), + baseProviderIcon: (props) => SpotifyIcon(props), + scopes: [ + 'user-read-private', + 'user-read-email', + 'user-library-read', + 'user-library-modify', + 'playlist-read-private', + 'playlist-read-collaborative', + 'playlist-modify-public', + 'playlist-modify-private', + 'user-read-playback-state', + 'user-modify-playback-state', + 'user-read-currently-playing', + 'user-read-recently-played', + 'user-top-read', + 'user-follow-read', + 'user-follow-modify', + 'user-read-playback-position', + 'ugc-image-upload', + ], + }, + }, + defaultService: 'spotify', + }, } /** @@ -1470,6 +1509,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } + case 'spotify': { + const { clientId, clientSecret } = getCredentials( + env.SPOTIFY_CLIENT_ID, + env.SPOTIFY_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://accounts.spotify.com/api/token', + clientId, + clientSecret, + useBasicAuth: true, + supportsRefreshTokenRotation: false, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/package.json b/apps/sim/package.json index c3e9d2795..83d469da3 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -20,7 +20,7 @@ "test:coverage": "vitest run --coverage", "email:dev": "email dev --dir components/emails", "type-check": "tsc --noEmit", - "test:billing:suite": "bun run scripts/test-billing-suite.ts" + "generate-docs": "bun run ../../scripts/generate-docs.ts" }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d11247605..ed28b15e1 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1014,6 +1014,86 @@ import { } from '@/tools/slack' import { smsSendTool } from '@/tools/sms' import { smtpSendMailTool } from '@/tools/smtp' +import { + spotifyAddPlaylistCoverTool, + spotifyAddToQueueTool, + spotifyAddTracksToPlaylistTool, + spotifyCheckFollowingTool, + spotifyCheckPlaylistFollowersTool, + spotifyCheckSavedAlbumsTool, + spotifyCheckSavedAudiobooksTool, + spotifyCheckSavedEpisodesTool, + spotifyCheckSavedShowsTool, + spotifyCheckSavedTracksTool, + spotifyCreatePlaylistTool, + spotifyFollowArtistsTool, + spotifyFollowPlaylistTool, + spotifyGetAlbumsTool, + spotifyGetAlbumTool, + spotifyGetAlbumTracksTool, + spotifyGetArtistAlbumsTool, + spotifyGetArtistsTool, + spotifyGetArtistTool, + spotifyGetArtistTopTracksTool, + spotifyGetAudiobookChaptersTool, + spotifyGetAudiobooksTool, + spotifyGetAudiobookTool, + spotifyGetCategoriesTool, + spotifyGetCurrentlyPlayingTool, + spotifyGetCurrentUserTool, + spotifyGetDevicesTool, + spotifyGetEpisodesTool, + spotifyGetEpisodeTool, + spotifyGetFollowedArtistsTool, + spotifyGetMarketsTool, + spotifyGetNewReleasesTool, + spotifyGetPlaybackStateTool, + spotifyGetPlaylistCoverTool, + spotifyGetPlaylistTool, + spotifyGetPlaylistTracksTool, + spotifyGetQueueTool, + spotifyGetRecentlyPlayedTool, + spotifyGetSavedAlbumsTool, + spotifyGetSavedAudiobooksTool, + spotifyGetSavedEpisodesTool, + spotifyGetSavedShowsTool, + spotifyGetSavedTracksTool, + spotifyGetShowEpisodesTool, + spotifyGetShowsTool, + spotifyGetShowTool, + spotifyGetTopArtistsTool, + spotifyGetTopTracksTool, + spotifyGetTracksTool, + spotifyGetTrackTool, + spotifyGetUserPlaylistsTool, + spotifyGetUserProfileTool, + spotifyPauseTool, + spotifyPlayTool, + spotifyRemoveSavedAlbumsTool, + spotifyRemoveSavedAudiobooksTool, + spotifyRemoveSavedEpisodesTool, + spotifyRemoveSavedShowsTool, + spotifyRemoveSavedTracksTool, + spotifyRemoveTracksFromPlaylistTool, + spotifyReorderPlaylistItemsTool, + spotifyReplacePlaylistItemsTool, + spotifySaveAlbumsTool, + spotifySaveAudiobooksTool, + spotifySaveEpisodesTool, + spotifySaveShowsTool, + spotifySaveTracksTool, + spotifySearchTool, + spotifySeekTool, + spotifySetRepeatTool, + spotifySetShuffleTool, + spotifySetVolumeTool, + spotifySkipNextTool, + spotifySkipPreviousTool, + spotifyTransferPlaybackTool, + spotifyUnfollowArtistsTool, + spotifyUnfollowPlaylistTool, + spotifyUpdatePlaylistTool, +} from '@/tools/spotify' import { sshCheckCommandExistsTool, sshCheckFileExistsTool, @@ -2429,4 +2509,83 @@ export const tools: Record = { zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool, zoom_delete_recording: zoomDeleteRecordingTool, zoom_list_past_participants: zoomListPastParticipantsTool, + // Spotify + spotify_search: spotifySearchTool, + spotify_get_track: spotifyGetTrackTool, + spotify_get_tracks: spotifyGetTracksTool, + spotify_get_album: spotifyGetAlbumTool, + spotify_get_albums: spotifyGetAlbumsTool, + spotify_get_album_tracks: spotifyGetAlbumTracksTool, + spotify_get_saved_albums: spotifyGetSavedAlbumsTool, + spotify_save_albums: spotifySaveAlbumsTool, + spotify_remove_saved_albums: spotifyRemoveSavedAlbumsTool, + spotify_check_saved_albums: spotifyCheckSavedAlbumsTool, + spotify_get_artist: spotifyGetArtistTool, + spotify_get_artists: spotifyGetArtistsTool, + spotify_get_artist_albums: spotifyGetArtistAlbumsTool, + spotify_get_artist_top_tracks: spotifyGetArtistTopTracksTool, + spotify_follow_artists: spotifyFollowArtistsTool, + spotify_unfollow_artists: spotifyUnfollowArtistsTool, + spotify_get_followed_artists: spotifyGetFollowedArtistsTool, + spotify_check_following: spotifyCheckFollowingTool, + spotify_get_show: spotifyGetShowTool, + spotify_get_shows: spotifyGetShowsTool, + spotify_get_show_episodes: spotifyGetShowEpisodesTool, + spotify_get_saved_shows: spotifyGetSavedShowsTool, + spotify_save_shows: spotifySaveShowsTool, + spotify_remove_saved_shows: spotifyRemoveSavedShowsTool, + spotify_check_saved_shows: spotifyCheckSavedShowsTool, + spotify_get_episode: spotifyGetEpisodeTool, + spotify_get_episodes: spotifyGetEpisodesTool, + spotify_get_saved_episodes: spotifyGetSavedEpisodesTool, + spotify_save_episodes: spotifySaveEpisodesTool, + spotify_remove_saved_episodes: spotifyRemoveSavedEpisodesTool, + spotify_check_saved_episodes: spotifyCheckSavedEpisodesTool, + spotify_get_audiobook: spotifyGetAudiobookTool, + spotify_get_audiobooks: spotifyGetAudiobooksTool, + spotify_get_audiobook_chapters: spotifyGetAudiobookChaptersTool, + spotify_get_saved_audiobooks: spotifyGetSavedAudiobooksTool, + spotify_save_audiobooks: spotifySaveAudiobooksTool, + spotify_remove_saved_audiobooks: spotifyRemoveSavedAudiobooksTool, + spotify_check_saved_audiobooks: spotifyCheckSavedAudiobooksTool, + spotify_get_playlist: spotifyGetPlaylistTool, + spotify_get_playlist_tracks: spotifyGetPlaylistTracksTool, + spotify_get_playlist_cover: spotifyGetPlaylistCoverTool, + spotify_get_user_playlists: spotifyGetUserPlaylistsTool, + spotify_create_playlist: spotifyCreatePlaylistTool, + spotify_update_playlist: spotifyUpdatePlaylistTool, + spotify_add_playlist_cover: spotifyAddPlaylistCoverTool, + spotify_add_tracks_to_playlist: spotifyAddTracksToPlaylistTool, + spotify_remove_tracks_from_playlist: spotifyRemoveTracksFromPlaylistTool, + spotify_reorder_playlist_items: spotifyReorderPlaylistItemsTool, + spotify_replace_playlist_items: spotifyReplacePlaylistItemsTool, + spotify_follow_playlist: spotifyFollowPlaylistTool, + spotify_unfollow_playlist: spotifyUnfollowPlaylistTool, + spotify_check_playlist_followers: spotifyCheckPlaylistFollowersTool, + spotify_get_current_user: spotifyGetCurrentUserTool, + spotify_get_user_profile: spotifyGetUserProfileTool, + spotify_get_top_tracks: spotifyGetTopTracksTool, + spotify_get_top_artists: spotifyGetTopArtistsTool, + spotify_get_saved_tracks: spotifyGetSavedTracksTool, + spotify_save_tracks: spotifySaveTracksTool, + spotify_remove_saved_tracks: spotifyRemoveSavedTracksTool, + spotify_check_saved_tracks: spotifyCheckSavedTracksTool, + spotify_get_recently_played: spotifyGetRecentlyPlayedTool, + spotify_get_new_releases: spotifyGetNewReleasesTool, + spotify_get_categories: spotifyGetCategoriesTool, + spotify_get_markets: spotifyGetMarketsTool, + spotify_get_playback_state: spotifyGetPlaybackStateTool, + spotify_get_currently_playing: spotifyGetCurrentlyPlayingTool, + spotify_get_devices: spotifyGetDevicesTool, + spotify_get_queue: spotifyGetQueueTool, + spotify_play: spotifyPlayTool, + spotify_pause: spotifyPauseTool, + spotify_skip_next: spotifySkipNextTool, + spotify_skip_previous: spotifySkipPreviousTool, + spotify_seek: spotifySeekTool, + spotify_add_to_queue: spotifyAddToQueueTool, + spotify_set_volume: spotifySetVolumeTool, + spotify_set_repeat: spotifySetRepeatTool, + spotify_set_shuffle: spotifySetShuffleTool, + spotify_transfer_playback: spotifyTransferPlaybackTool, } diff --git a/apps/sim/tools/spotify/add_playlist_cover.ts b/apps/sim/tools/spotify/add_playlist_cover.ts new file mode 100644 index 000000000..551958378 --- /dev/null +++ b/apps/sim/tools/spotify/add_playlist_cover.ts @@ -0,0 +1,58 @@ +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, + description: 'The Spotify playlist ID', + }, + imageBase64: { + type: 'string', + required: true, + description: 'Base64-encoded JPEG image (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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether upload succeeded' }, + }, +} diff --git a/apps/sim/tools/spotify/add_to_queue.ts b/apps/sim/tools/spotify/add_to_queue.ts new file mode 100644 index 000000000..7753fe123 --- /dev/null +++ b/apps/sim/tools/spotify/add_to_queue.ts @@ -0,0 +1,59 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyAddToQueueParams, SpotifyAddToQueueResponse } from './types' + +export const spotifyAddToQueueTool: ToolConfig = + { + 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 => { + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether track was added to queue' }, + }, + } diff --git a/apps/sim/tools/spotify/add_tracks_to_playlist.ts b/apps/sim/tools/spotify/add_tracks_to_playlist.ts new file mode 100644 index 000000000..0fbd2a610 --- /dev/null +++ b/apps/sim/tools/spotify/add_tracks_to_playlist.ts @@ -0,0 +1,71 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyAddTracksToPlaylistParams, SpotifyAddTracksToPlaylistResponse } from './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-only', + description: 'Position to insert tracks (0-based). 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/check_following.ts b/apps/sim/tools/spotify/check_following.ts new file mode 100644 index 000000000..c1bcca245 --- /dev/null +++ b/apps/sim/tools/spotify/check_following.ts @@ -0,0 +1,64 @@ +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, + description: 'Type to check: "artist" or "user"', + }, + ids: { + type: 'string', + required: true, + 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 => { + const results = await response.json() + return { success: true, output: { results } } + }, + + outputs: { + results: { type: 'json', description: 'Array of booleans for each ID' }, + }, +} diff --git a/apps/sim/tools/spotify/check_playlist_followers.ts b/apps/sim/tools/spotify/check_playlist_followers.ts new file mode 100644 index 000000000..973647563 --- /dev/null +++ b/apps/sim/tools/spotify/check_playlist_followers.ts @@ -0,0 +1,64 @@ +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, + description: 'The Spotify playlist ID', + }, + userIds: { + type: 'string', + required: true, + 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 => { + const results = await response.json() + return { success: true, output: { results } } + }, + + outputs: { + results: { type: 'json', description: 'Array of booleans for each user' }, + }, +} diff --git a/apps/sim/tools/spotify/check_saved_albums.ts b/apps/sim/tools/spotify/check_saved_albums.ts new file mode 100644 index 000000000..751d6260c --- /dev/null +++ b/apps/sim/tools/spotify/check_saved_albums.ts @@ -0,0 +1,58 @@ +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, + 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 => { + const results = await response.json() + return { success: true, output: { results } } + }, + + outputs: { + results: { type: 'json', description: 'Array of booleans for each album' }, + }, +} diff --git a/apps/sim/tools/spotify/check_saved_audiobooks.ts b/apps/sim/tools/spotify/check_saved_audiobooks.ts new file mode 100644 index 000000000..2c14ebe01 --- /dev/null +++ b/apps/sim/tools/spotify/check_saved_audiobooks.ts @@ -0,0 +1,58 @@ +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, + 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 => { + const results = await response.json() + return { success: true, output: { results } } + }, + + outputs: { + results: { type: 'json', description: 'Array of booleans for each audiobook' }, + }, +} diff --git a/apps/sim/tools/spotify/check_saved_episodes.ts b/apps/sim/tools/spotify/check_saved_episodes.ts new file mode 100644 index 000000000..45027e1dd --- /dev/null +++ b/apps/sim/tools/spotify/check_saved_episodes.ts @@ -0,0 +1,58 @@ +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, + 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 => { + const results = await response.json() + return { success: true, output: { results } } + }, + + outputs: { + results: { type: 'json', description: 'Array of booleans for each episode' }, + }, +} diff --git a/apps/sim/tools/spotify/check_saved_shows.ts b/apps/sim/tools/spotify/check_saved_shows.ts new file mode 100644 index 000000000..22efd1e2f --- /dev/null +++ b/apps/sim/tools/spotify/check_saved_shows.ts @@ -0,0 +1,58 @@ +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, + 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 => { + const results = await response.json() + return { success: true, output: { results } } + }, + + outputs: { + results: { type: 'json', description: 'Array of booleans for each show' }, + }, +} diff --git a/apps/sim/tools/spotify/check_saved_tracks.ts b/apps/sim/tools/spotify/check_saved_tracks.ts new file mode 100644 index 000000000..0793d6e72 --- /dev/null +++ b/apps/sim/tools/spotify/check_saved_tracks.ts @@ -0,0 +1,71 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyCheckSavedTracksParams, SpotifyCheckSavedTracksResponse } from './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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/create_playlist.ts b/apps/sim/tools/spotify/create_playlist.ts new file mode 100644 index 000000000..c28cc0d2e --- /dev/null +++ b/apps/sim/tools/spotify/create_playlist.ts @@ -0,0 +1,89 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyCreatePlaylistParams, SpotifyCreatePlaylistResponse } from './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-only', + default: true, + description: 'Whether the playlist should be public', + }, + collaborative: { + type: 'boolean', + required: false, + visibility: 'user-only', + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/follow_artists.ts b/apps/sim/tools/spotify/follow_artists.ts new file mode 100644 index 000000000..c1f91bf1c --- /dev/null +++ b/apps/sim/tools/spotify/follow_artists.ts @@ -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 => { + return { + success: true, + output: { success: true }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether artists were followed successfully' }, + }, +} diff --git a/apps/sim/tools/spotify/follow_playlist.ts b/apps/sim/tools/spotify/follow_playlist.ts new file mode 100644 index 000000000..62f2c6ea4 --- /dev/null +++ b/apps/sim/tools/spotify/follow_playlist.ts @@ -0,0 +1,61 @@ +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, + description: 'The Spotify playlist ID', + }, + public: { + type: 'boolean', + required: false, + default: true, + description: 'Whether the playlist will be 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether follow succeeded' }, + }, +} diff --git a/apps/sim/tools/spotify/get_album.ts b/apps/sim/tools/spotify/get_album.ts new file mode 100644 index 000000000..7c85d3ce1 --- /dev/null +++ b/apps/sim/tools/spotify/get_album.ts @@ -0,0 +1,87 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetAlbumParams, SpotifyGetAlbumResponse } from './types' + +export const spotifyGetAlbumTool: ToolConfig = { + 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-only', + description: 'ISO 3166-1 alpha-2 country code for track availability', + }, + }, + + 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 => { + 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' }, + 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' }, + external_url: { type: 'string', description: 'Spotify URL' }, + }, +} diff --git a/apps/sim/tools/spotify/get_album_tracks.ts b/apps/sim/tools/spotify/get_album_tracks.ts new file mode 100644 index 000000000..c64830798 --- /dev/null +++ b/apps/sim/tools/spotify/get_album_tracks.ts @@ -0,0 +1,112 @@ +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-only', + default: 20, + description: 'Number of tracks to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of first track to return', + }, + }, + + 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 => { + 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: 'json', description: 'List of tracks' }, + total: { type: 'number', description: 'Total number of tracks' }, + next: { type: 'string', description: 'URL for next page', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_albums.ts b/apps/sim/tools/spotify/get_albums.ts new file mode 100644 index 000000000..2d6bcabc8 --- /dev/null +++ b/apps/sim/tools/spotify/get_albums.ts @@ -0,0 +1,94 @@ +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 = { + 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-only', + description: 'ISO country code for market', + }, + }, + + 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 => { + 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: 'json', description: 'List of albums' }, + }, +} diff --git a/apps/sim/tools/spotify/get_artist.ts b/apps/sim/tools/spotify/get_artist.ts new file mode 100644 index 000000000..d8f223c7e --- /dev/null +++ b/apps/sim/tools/spotify/get_artist.ts @@ -0,0 +1,59 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetArtistParams, SpotifyGetArtistResponse } from './types' + +export const spotifyGetArtistTool: ToolConfig = { + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_artist_albums.ts b/apps/sim/tools/spotify/get_artist_albums.ts new file mode 100644 index 000000000..3498bfb95 --- /dev/null +++ b/apps/sim/tools/spotify/get_artist_albums.ts @@ -0,0 +1,116 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetArtistAlbumsParams, SpotifyGetArtistAlbumsResponse } from './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-only', + default: 20, + description: 'Maximum number of albums to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of the first album to return', + }, + market: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'ISO 3166-1 alpha-2 country code', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_artist_top_tracks.ts b/apps/sim/tools/spotify/get_artist_top_tracks.ts new file mode 100644 index 000000000..3b65203e4 --- /dev/null +++ b/apps/sim/tools/spotify/get_artist_top_tracks.ts @@ -0,0 +1,89 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetArtistTopTracksParams, SpotifyGetArtistTopTracksResponse } from './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-only', + default: 'US', + description: 'ISO 3166-1 alpha-2 country code (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 => { + 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: { + id: { type: 'string', description: 'Spotify track ID' }, + name: { type: 'string', description: 'Track name' }, + album: { type: 'object', description: 'Album information' }, + duration_ms: { type: 'number', description: 'Track duration in milliseconds' }, + popularity: { type: 'number', description: 'Popularity score (0-100)' }, + preview_url: { type: 'string', description: 'URL to 30-second preview' }, + external_url: { type: 'string', description: 'Spotify URL' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/spotify/get_artists.ts b/apps/sim/tools/spotify/get_artists.ts new file mode 100644 index 000000000..62e69e35d --- /dev/null +++ b/apps/sim/tools/spotify/get_artists.ts @@ -0,0 +1,82 @@ +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 = + { + 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 => { + 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: 'json', description: 'List of artists' }, + }, + } diff --git a/apps/sim/tools/spotify/get_audiobook.ts b/apps/sim/tools/spotify/get_audiobook.ts new file mode 100644 index 000000000..526843208 --- /dev/null +++ b/apps/sim/tools/spotify/get_audiobook.ts @@ -0,0 +1,95 @@ +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, + description: 'The Spotify audiobook ID', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_audiobook_chapters.ts b/apps/sim/tools/spotify/get_audiobook_chapters.ts new file mode 100644 index 000000000..451a082ce --- /dev/null +++ b/apps/sim/tools/spotify/get_audiobook_chapters.ts @@ -0,0 +1,104 @@ +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, + description: 'The Spotify audiobook ID', + }, + limit: { + type: 'number', + required: false, + default: 20, + description: 'Number of chapters to return (1-50)', + }, + offset: { + type: 'number', + required: false, + default: 0, + description: 'Index of first chapter to return', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_audiobooks.ts b/apps/sim/tools/spotify/get_audiobooks.ts new file mode 100644 index 000000000..3e5519b36 --- /dev/null +++ b/apps/sim/tools/spotify/get_audiobooks.ts @@ -0,0 +1,87 @@ +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, + description: 'Comma-separated audiobook IDs (max 50)', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_categories.ts b/apps/sim/tools/spotify/get_categories.ts new file mode 100644 index 000000000..4a153e37a --- /dev/null +++ b/apps/sim/tools/spotify/get_categories.ts @@ -0,0 +1,82 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetCategoriesParams, SpotifyGetCategoriesResponse } from './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-only', + description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")', + }, + locale: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Locale code (e.g., "en_US", "es_MX")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_current_user.ts b/apps/sim/tools/spotify/get_current_user.ts new file mode 100644 index 000000000..a9d95917f --- /dev/null +++ b/apps/sim/tools/spotify/get_current_user.ts @@ -0,0 +1,58 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetCurrentUserParams, SpotifyGetCurrentUserResponse } from './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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_currently_playing.ts b/apps/sim/tools/spotify/get_currently_playing.ts new file mode 100644 index 000000000..0a679c268 --- /dev/null +++ b/apps/sim/tools/spotify/get_currently_playing.ts @@ -0,0 +1,108 @@ +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-only', + description: 'ISO country code for market', + }, + }, + + 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 => { + 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: 'json', description: 'Currently playing track', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_devices.ts b/apps/sim/tools/spotify/get_devices.ts new file mode 100644 index 000000000..ff8048a6d --- /dev/null +++ b/apps/sim/tools/spotify/get_devices.ts @@ -0,0 +1,67 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetDevicesParams, SpotifyGetDevicesResponse } from './types' + +export const spotifyGetDevicesTool: ToolConfig = + { + 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 => { + 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)' }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/spotify/get_episode.ts b/apps/sim/tools/spotify/get_episode.ts new file mode 100644 index 000000000..c6a0e373c --- /dev/null +++ b/apps/sim/tools/spotify/get_episode.ts @@ -0,0 +1,94 @@ +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 = + { + 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, + description: 'The Spotify episode ID', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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' }, + }, + } diff --git a/apps/sim/tools/spotify/get_episodes.ts b/apps/sim/tools/spotify/get_episodes.ts new file mode 100644 index 000000000..63555f0ee --- /dev/null +++ b/apps/sim/tools/spotify/get_episodes.ts @@ -0,0 +1,91 @@ +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, + description: 'Comma-separated episode IDs (max 50)', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_followed_artists.ts b/apps/sim/tools/spotify/get_followed_artists.ts new file mode 100644 index 000000000..1cbdaea15 --- /dev/null +++ b/apps/sim/tools/spotify/get_followed_artists.ts @@ -0,0 +1,100 @@ +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-only', + default: 20, + description: 'Number of artists to return (1-50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-only', + 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 => { + 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: 'json', description: 'List of followed artists' }, + total: { type: 'number', description: 'Total number of followed artists' }, + next: { type: 'string', description: 'Cursor for next page', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_markets.ts b/apps/sim/tools/spotify/get_markets.ts new file mode 100644 index 000000000..5e29c3614 --- /dev/null +++ b/apps/sim/tools/spotify/get_markets.ts @@ -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 = + { + 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 => { + const data = await response.json() + return { + success: true, + output: { markets: data.markets || [] }, + } + }, + + outputs: { + markets: { type: 'json', description: 'List of ISO country codes' }, + }, + } diff --git a/apps/sim/tools/spotify/get_new_releases.ts b/apps/sim/tools/spotify/get_new_releases.ts new file mode 100644 index 000000000..9f2c10a40 --- /dev/null +++ b/apps/sim/tools/spotify/get_new_releases.ts @@ -0,0 +1,88 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetNewReleasesParams, SpotifyGetNewReleasesResponse } from './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-only', + description: 'ISO 3166-1 alpha-2 country code (e.g., "US", "GB")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + default: 20, + description: 'Number of releases to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of first release to return', + }, + }, + + 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 => { + 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: 'json', description: 'List of new releases' }, + total: { type: 'number', description: 'Total number of new releases' }, + next: { type: 'string', description: 'URL for next page', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_playback_state.ts b/apps/sim/tools/spotify/get_playback_state.ts new file mode 100644 index 000000000..b0e9c2152 --- /dev/null +++ b/apps/sim/tools/spotify/get_playback_state.ts @@ -0,0 +1,103 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetPlaybackStateParams, SpotifyGetPlaybackStateResponse } from './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-only', + description: 'ISO 3166-1 alpha-2 country code', + }, + }, + + 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 => { + 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 }, + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_playlist.ts b/apps/sim/tools/spotify/get_playlist.ts new file mode 100644 index 000000000..b1b0a84ab --- /dev/null +++ b/apps/sim/tools/spotify/get_playlist.ts @@ -0,0 +1,83 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetPlaylistParams, SpotifyGetPlaylistResponse } from './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-only', + description: 'ISO 3166-1 alpha-2 country code for track availability', + }, + }, + + 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 => { + 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' }, + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_playlist_cover.ts b/apps/sim/tools/spotify/get_playlist_cover.ts new file mode 100644 index 000000000..9650d5aff --- /dev/null +++ b/apps/sim/tools/spotify/get_playlist_cover.ts @@ -0,0 +1,66 @@ +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, + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_playlist_tracks.ts b/apps/sim/tools/spotify/get_playlist_tracks.ts new file mode 100644 index 000000000..8bd423efd --- /dev/null +++ b/apps/sim/tools/spotify/get_playlist_tracks.ts @@ -0,0 +1,113 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetPlaylistTracksParams, SpotifyGetPlaylistTracksResponse } from './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-only', + default: 50, + description: 'Maximum number of tracks to return (1-100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of the first track to return', + }, + market: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'ISO 3166-1 alpha-2 country code for track availability', + }, + }, + + 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 => { + 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' }, + }, + }, + }, + total: { type: 'number', description: 'Total number of tracks in playlist' }, + next: { type: 'string', description: 'URL for next page of results', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_queue.ts b/apps/sim/tools/spotify/get_queue.ts new file mode 100644 index 000000000..ef36bd79d --- /dev/null +++ b/apps/sim/tools/spotify/get_queue.ts @@ -0,0 +1,85 @@ +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 = { + 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 => { + 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: 'json', description: 'Currently playing track', optional: true }, + queue: { type: 'json', description: 'Upcoming tracks in queue' }, + }, +} diff --git a/apps/sim/tools/spotify/get_recently_played.ts b/apps/sim/tools/spotify/get_recently_played.ts new file mode 100644 index 000000000..51bd7f976 --- /dev/null +++ b/apps/sim/tools/spotify/get_recently_played.ts @@ -0,0 +1,102 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetRecentlyPlayedParams, SpotifyGetRecentlyPlayedResponse } from './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-only', + default: 20, + description: 'Number of tracks to return (1-50)', + }, + after: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Unix timestamp in milliseconds. Returns items after this cursor.', + }, + before: { + type: 'number', + required: false, + visibility: 'user-only', + 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 => { + 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' }, + }, + }, + }, + next: { type: 'string', description: 'URL for next page', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_saved_albums.ts b/apps/sim/tools/spotify/get_saved_albums.ts new file mode 100644 index 000000000..d35023501 --- /dev/null +++ b/apps/sim/tools/spotify/get_saved_albums.ts @@ -0,0 +1,106 @@ +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, + default: 20, + description: 'Number of albums to return (1-50)', + }, + offset: { + type: 'number', + required: false, + default: 0, + description: 'Index of first album to return', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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: 'json', description: 'List of saved albums' }, + total: { type: 'number', description: 'Total saved albums' }, + next: { type: 'string', description: 'URL for next page', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_saved_audiobooks.ts b/apps/sim/tools/spotify/get_saved_audiobooks.ts new file mode 100644 index 000000000..fcb7b83de --- /dev/null +++ b/apps/sim/tools/spotify/get_saved_audiobooks.ts @@ -0,0 +1,97 @@ +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, + default: 20, + description: 'Number of audiobooks to return (1-50)', + }, + offset: { + type: 'number', + required: false, + default: 0, + description: 'Index of first audiobook to return', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_saved_episodes.ts b/apps/sim/tools/spotify/get_saved_episodes.ts new file mode 100644 index 000000000..a9dec882e --- /dev/null +++ b/apps/sim/tools/spotify/get_saved_episodes.ts @@ -0,0 +1,106 @@ +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, + default: 20, + description: 'Number of episodes to return (1-50)', + }, + offset: { + type: 'number', + required: false, + default: 0, + description: 'Index of first episode to return', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_saved_shows.ts b/apps/sim/tools/spotify/get_saved_shows.ts new file mode 100644 index 000000000..e0b9cf6f8 --- /dev/null +++ b/apps/sim/tools/spotify/get_saved_shows.ts @@ -0,0 +1,96 @@ +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, + default: 20, + description: 'Number of shows to return (1-50)', + }, + offset: { + type: 'number', + required: false, + default: 0, + description: 'Index of first show to return', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_saved_tracks.ts b/apps/sim/tools/spotify/get_saved_tracks.ts new file mode 100644 index 000000000..61f07f1a7 --- /dev/null +++ b/apps/sim/tools/spotify/get_saved_tracks.ts @@ -0,0 +1,104 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetSavedTracksParams, SpotifyGetSavedTracksResponse } from './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-only', + default: 20, + description: 'Number of tracks to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of the first track to return', + }, + market: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'ISO 3166-1 alpha-2 country code', + }, + }, + + 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 => { + 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' }, + }, + }, + }, + total: { type: 'number', description: 'Total number of saved tracks' }, + next: { type: 'string', description: 'URL for next page', optional: true }, + }, +} diff --git a/apps/sim/tools/spotify/get_show.ts b/apps/sim/tools/spotify/get_show.ts new file mode 100644 index 000000000..66e58445a --- /dev/null +++ b/apps/sim/tools/spotify/get_show.ts @@ -0,0 +1,89 @@ +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 = { + 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, + description: 'The Spotify show ID', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_show_episodes.ts b/apps/sim/tools/spotify/get_show_episodes.ts new file mode 100644 index 000000000..9c290cd38 --- /dev/null +++ b/apps/sim/tools/spotify/get_show_episodes.ts @@ -0,0 +1,106 @@ +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, + description: 'The Spotify show ID', + }, + limit: { + type: 'number', + required: false, + default: 20, + description: 'Number of episodes to return (1-50)', + }, + offset: { + type: 'number', + required: false, + default: 0, + description: 'Index of first episode to return', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_shows.ts b/apps/sim/tools/spotify/get_shows.ts new file mode 100644 index 000000000..99ff862b6 --- /dev/null +++ b/apps/sim/tools/spotify/get_shows.ts @@ -0,0 +1,84 @@ +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 = { + 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, + description: 'Comma-separated show IDs (max 50)', + }, + market: { + type: 'string', + required: false, + description: 'ISO country code for market', + }, + }, + + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_top_artists.ts b/apps/sim/tools/spotify/get_top_artists.ts new file mode 100644 index 000000000..7c8d453f6 --- /dev/null +++ b/apps/sim/tools/spotify/get_top_artists.ts @@ -0,0 +1,100 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetTopArtistsResponse, SpotifyGetTopItemsParams } from './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-only', + default: 20, + description: 'Number of artists to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of the first artist to return', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_top_tracks.ts b/apps/sim/tools/spotify/get_top_tracks.ts new file mode 100644 index 000000000..f63dbc104 --- /dev/null +++ b/apps/sim/tools/spotify/get_top_tracks.ts @@ -0,0 +1,104 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetTopItemsParams, SpotifyGetTopTracksResponse } from './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-only', + default: 20, + description: 'Number of tracks to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of the first track to return', + }, + }, + + 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 => { + 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' }, + album: { type: 'object', description: 'Album information' }, + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_track.ts b/apps/sim/tools/spotify/get_track.ts new file mode 100644 index 000000000..b6360efa1 --- /dev/null +++ b/apps/sim/tools/spotify/get_track.ts @@ -0,0 +1,81 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetTrackParams, SpotifyGetTrackResponse } from './types' + +export const spotifyGetTrackTool: ToolConfig = { + 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-only', + description: 'ISO 3166-1 alpha-2 country code for track availability', + }, + }, + + 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 => { + 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' }, + album: { type: 'object', description: 'Album information' }, + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/get_tracks.ts b/apps/sim/tools/spotify/get_tracks.ts new file mode 100644 index 000000000..6c6d3312d --- /dev/null +++ b/apps/sim/tools/spotify/get_tracks.ts @@ -0,0 +1,94 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetTracksParams, SpotifyGetTracksResponse } from './types' + +export const spotifyGetTracksTool: ToolConfig = { + 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-only', + description: 'ISO 3166-1 alpha-2 country code for track availability', + }, + }, + + 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 => { + 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' }, + album: { type: 'object', description: 'Album information' }, + 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' }, + external_url: { type: 'string', description: 'Spotify URL' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/spotify/get_user_playlists.ts b/apps/sim/tools/spotify/get_user_playlists.ts new file mode 100644 index 000000000..f799e4507 --- /dev/null +++ b/apps/sim/tools/spotify/get_user_playlists.ts @@ -0,0 +1,96 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyGetUserPlaylistsParams, SpotifyGetUserPlaylistsResponse } from './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-only', + default: 20, + description: 'Maximum number of playlists to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of the first playlist to return', + }, + }, + + 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 => { + 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 }, + }, +} diff --git a/apps/sim/tools/spotify/get_user_profile.ts b/apps/sim/tools/spotify/get_user_profile.ts new file mode 100644 index 000000000..43a47930d --- /dev/null +++ b/apps/sim/tools/spotify/get_user_profile.ts @@ -0,0 +1,70 @@ +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, + 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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/index.ts b/apps/sim/tools/spotify/index.ts new file mode 100644 index 000000000..cc171a4d7 --- /dev/null +++ b/apps/sim/tools/spotify/index.ts @@ -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' diff --git a/apps/sim/tools/spotify/pause.ts b/apps/sim/tools/spotify/pause.ts new file mode 100644 index 000000000..7bac7d5bb --- /dev/null +++ b/apps/sim/tools/spotify/pause.ts @@ -0,0 +1,52 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyPauseParams, SpotifyPauseResponse } from './types' + +export const spotifyPauseTool: ToolConfig = { + 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 => { + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether playback was paused' }, + }, +} diff --git a/apps/sim/tools/spotify/play.ts b/apps/sim/tools/spotify/play.ts new file mode 100644 index 000000000..1b304be6a --- /dev/null +++ b/apps/sim/tools/spotify/play.ts @@ -0,0 +1,94 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifyPlayParams, SpotifyPlayResponse } from './types' + +export const spotifyPlayTool: ToolConfig = { + 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-only', + description: 'Position in context to start playing (0-based index)', + }, + position_ms: { + type: 'number', + required: false, + visibility: 'user-only', + 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 => { + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether playback started successfully' }, + }, +} diff --git a/apps/sim/tools/spotify/remove_saved_albums.ts b/apps/sim/tools/spotify/remove_saved_albums.ts new file mode 100644 index 000000000..fe5274dec --- /dev/null +++ b/apps/sim/tools/spotify/remove_saved_albums.ts @@ -0,0 +1,57 @@ +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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether albums were removed' }, + }, +} diff --git a/apps/sim/tools/spotify/remove_saved_audiobooks.ts b/apps/sim/tools/spotify/remove_saved_audiobooks.ts new file mode 100644 index 000000000..58518e548 --- /dev/null +++ b/apps/sim/tools/spotify/remove_saved_audiobooks.ts @@ -0,0 +1,57 @@ +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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether audiobooks were removed' }, + }, +} diff --git a/apps/sim/tools/spotify/remove_saved_episodes.ts b/apps/sim/tools/spotify/remove_saved_episodes.ts new file mode 100644 index 000000000..62af65082 --- /dev/null +++ b/apps/sim/tools/spotify/remove_saved_episodes.ts @@ -0,0 +1,57 @@ +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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether episodes were removed' }, + }, +} diff --git a/apps/sim/tools/spotify/remove_saved_shows.ts b/apps/sim/tools/spotify/remove_saved_shows.ts new file mode 100644 index 000000000..f12da679b --- /dev/null +++ b/apps/sim/tools/spotify/remove_saved_shows.ts @@ -0,0 +1,57 @@ +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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether shows were removed' }, + }, +} diff --git a/apps/sim/tools/spotify/remove_saved_tracks.ts b/apps/sim/tools/spotify/remove_saved_tracks.ts new file mode 100644 index 000000000..0dac9aa87 --- /dev/null +++ b/apps/sim/tools/spotify/remove_saved_tracks.ts @@ -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 => { + return { + success: true, + output: { success: true }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether tracks were removed successfully' }, + }, +} diff --git a/apps/sim/tools/spotify/remove_tracks_from_playlist.ts b/apps/sim/tools/spotify/remove_tracks_from_playlist.ts new file mode 100644 index 000000000..222413f59 --- /dev/null +++ b/apps/sim/tools/spotify/remove_tracks_from_playlist.ts @@ -0,0 +1,65 @@ +import type { ToolConfig } from '@/tools/types' +import type { + SpotifyRemoveTracksFromPlaylistParams, + SpotifyRemoveTracksFromPlaylistResponse, +} from './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 => { + 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' }, + }, +} diff --git a/apps/sim/tools/spotify/reorder_playlist_items.ts b/apps/sim/tools/spotify/reorder_playlist_items.ts new file mode 100644 index 000000000..f0287088a --- /dev/null +++ b/apps/sim/tools/spotify/reorder_playlist_items.ts @@ -0,0 +1,91 @@ +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, + description: 'The Spotify playlist ID', + }, + range_start: { + type: 'number', + required: true, + description: 'Start index of items to reorder', + }, + insert_before: { + type: 'number', + required: true, + description: 'Index to insert items before', + }, + range_length: { + type: 'number', + required: false, + default: 1, + description: 'Number of items to reorder', + }, + snapshot_id: { + type: 'string', + required: false, + description: 'Playlist snapshot ID for concurrency control', + }, + }, + + 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 = { + 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 => { + const data = await response.json() + return { + success: true, + output: { snapshot_id: data.snapshot_id || '' }, + } + }, + + outputs: { + snapshot_id: { type: 'string', description: 'New playlist snapshot ID' }, + }, +} diff --git a/apps/sim/tools/spotify/replace_playlist_items.ts b/apps/sim/tools/spotify/replace_playlist_items.ts new file mode 100644 index 000000000..0bd278e24 --- /dev/null +++ b/apps/sim/tools/spotify/replace_playlist_items.ts @@ -0,0 +1,69 @@ +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, + description: 'The Spotify playlist ID', + }, + uris: { + type: 'string', + required: true, + 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 => { + const data = await response.json() + return { + success: true, + output: { snapshot_id: data.snapshot_id || '' }, + } + }, + + outputs: { + snapshot_id: { type: 'string', description: 'New playlist snapshot ID' }, + }, +} diff --git a/apps/sim/tools/spotify/save_albums.ts b/apps/sim/tools/spotify/save_albums.ts new file mode 100644 index 000000000..414967a36 --- /dev/null +++ b/apps/sim/tools/spotify/save_albums.ts @@ -0,0 +1,55 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface SpotifySaveAlbumsParams { + accessToken: string + albumIds: string +} + +interface SpotifySaveAlbumsResponse extends ToolResponse { + output: { success: boolean } +} + +export const spotifySaveAlbumsTool: ToolConfig = + { + 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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether albums were saved' }, + }, + } diff --git a/apps/sim/tools/spotify/save_audiobooks.ts b/apps/sim/tools/spotify/save_audiobooks.ts new file mode 100644 index 000000000..e9d028e5e --- /dev/null +++ b/apps/sim/tools/spotify/save_audiobooks.ts @@ -0,0 +1,57 @@ +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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether audiobooks were saved' }, + }, +} diff --git a/apps/sim/tools/spotify/save_episodes.ts b/apps/sim/tools/spotify/save_episodes.ts new file mode 100644 index 000000000..4669bd308 --- /dev/null +++ b/apps/sim/tools/spotify/save_episodes.ts @@ -0,0 +1,57 @@ +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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether episodes were saved' }, + }, +} diff --git a/apps/sim/tools/spotify/save_shows.ts b/apps/sim/tools/spotify/save_shows.ts new file mode 100644 index 000000000..a6c2b196a --- /dev/null +++ b/apps/sim/tools/spotify/save_shows.ts @@ -0,0 +1,54 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface SpotifySaveShowsParams { + accessToken: string + showIds: string +} + +interface SpotifySaveShowsResponse extends ToolResponse { + output: { success: boolean } +} + +export const spotifySaveShowsTool: ToolConfig = { + 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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether shows were saved' }, + }, +} diff --git a/apps/sim/tools/spotify/save_tracks.ts b/apps/sim/tools/spotify/save_tracks.ts new file mode 100644 index 000000000..f639720d2 --- /dev/null +++ b/apps/sim/tools/spotify/save_tracks.ts @@ -0,0 +1,48 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifySaveTracksParams, SpotifySaveTracksResponse } from './types' + +export const spotifySaveTracksTool: ToolConfig = + { + 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 => { + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the tracks were saved successfully' }, + }, + } diff --git a/apps/sim/tools/spotify/search.ts b/apps/sim/tools/spotify/search.ts new file mode 100644 index 000000000..028906042 --- /dev/null +++ b/apps/sim/tools/spotify/search.ts @@ -0,0 +1,157 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifySearchParams, SpotifySearchResponse } from './types' + +export const spotifySearchTool: ToolConfig = { + 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-only', + default: 20, + description: 'Maximum number of results to return (1-50)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + default: 0, + description: 'Index of the first result to return for pagination', + }, + market: { + type: 'string', + required: false, + visibility: 'user-only', + 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 => { + 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: { + id: { type: 'string', description: 'Spotify track ID' }, + name: { type: 'string', description: 'Track name' }, + artists: { type: 'array', description: 'List of artist names' }, + album: { type: 'string', description: 'Album name' }, + duration_ms: { type: 'number', description: 'Track duration in milliseconds' }, + popularity: { type: 'number', description: 'Popularity score (0-100)' }, + preview_url: { type: 'string', description: 'URL to 30-second preview' }, + external_url: { type: 'string', description: 'Spotify URL' }, + }, + }, + }, + artists: { + type: 'array', + description: 'List of matching artists', + }, + albums: { + type: 'array', + description: 'List of matching albums', + }, + playlists: { + type: 'array', + description: 'List of matching playlists', + }, + }, +} diff --git a/apps/sim/tools/spotify/seek.ts b/apps/sim/tools/spotify/seek.ts new file mode 100644 index 000000000..049525a2a --- /dev/null +++ b/apps/sim/tools/spotify/seek.ts @@ -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 = { + 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-only', + description: 'Device ID to target', + }, + }, + + 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 => { + return { + success: true, + output: { success: true }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether seek was successful' }, + }, +} diff --git a/apps/sim/tools/spotify/set_repeat.ts b/apps/sim/tools/spotify/set_repeat.ts new file mode 100644 index 000000000..02e85fa50 --- /dev/null +++ b/apps/sim/tools/spotify/set_repeat.ts @@ -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 = { + 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-only', + description: 'Device ID to target', + }, + }, + + 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 => { + return { + success: true, + output: { success: true }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether repeat mode was set successfully' }, + }, +} diff --git a/apps/sim/tools/spotify/set_shuffle.ts b/apps/sim/tools/spotify/set_shuffle.ts new file mode 100644 index 000000000..e84c3c645 --- /dev/null +++ b/apps/sim/tools/spotify/set_shuffle.ts @@ -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 = + { + 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-only', + description: 'Device ID to target', + }, + }, + + 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 => { + return { + success: true, + output: { success: true }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether shuffle was set successfully' }, + }, + } diff --git a/apps/sim/tools/spotify/set_volume.ts b/apps/sim/tools/spotify/set_volume.ts new file mode 100644 index 000000000..045426e5f --- /dev/null +++ b/apps/sim/tools/spotify/set_volume.ts @@ -0,0 +1,59 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifySetVolumeParams, SpotifySetVolumeResponse } from './types' + +export const spotifySetVolumeTool: ToolConfig = { + 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 => { + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether volume was set' }, + }, +} diff --git a/apps/sim/tools/spotify/skip_next.ts b/apps/sim/tools/spotify/skip_next.ts new file mode 100644 index 000000000..21e4a7d3a --- /dev/null +++ b/apps/sim/tools/spotify/skip_next.ts @@ -0,0 +1,52 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifySkipNextParams, SpotifySkipNextResponse } from './types' + +export const spotifySkipNextTool: ToolConfig = { + 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 => { + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether skip was successful' }, + }, +} diff --git a/apps/sim/tools/spotify/skip_previous.ts b/apps/sim/tools/spotify/skip_previous.ts new file mode 100644 index 000000000..424676501 --- /dev/null +++ b/apps/sim/tools/spotify/skip_previous.ts @@ -0,0 +1,55 @@ +import type { ToolConfig } from '@/tools/types' +import type { SpotifySkipPreviousParams, SpotifySkipPreviousResponse } from './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 => { + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether skip was successful' }, + }, +} diff --git a/apps/sim/tools/spotify/transfer_playback.ts b/apps/sim/tools/spotify/transfer_playback.ts new file mode 100644 index 000000000..0f89da96d --- /dev/null +++ b/apps/sim/tools/spotify/transfer_playback.ts @@ -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-only', + 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 => { + return { + success: true, + output: { success: true }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether transfer was successful' }, + }, +} diff --git a/apps/sim/tools/spotify/types.ts b/apps/sim/tools/spotify/types.ts new file mode 100644 index 000000000..a8844ee09 --- /dev/null +++ b/apps/sim/tools/spotify/types.ts @@ -0,0 +1,1031 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Base params that include OAuth access token + */ +export interface SpotifyBaseParams { + accessToken: string +} + +/** + * Common Spotify objects + */ +export interface SpotifyImage { + url: string + height: number | null + width: number | null +} + +export interface SpotifyExternalUrls { + spotify: string +} + +export interface SpotifyArtistSimplified { + id: string + name: string + external_urls: SpotifyExternalUrls +} + +export interface SpotifyAlbumSimplified { + id: string + name: string + album_type: string + total_tracks: number + release_date: string + images: SpotifyImage[] + artists: SpotifyArtistSimplified[] + external_urls: SpotifyExternalUrls +} + +export interface SpotifyTrack { + id: string + name: string + duration_ms: number + explicit: boolean + popularity: number + preview_url: string | null + track_number: number + disc_number: number + album: SpotifyAlbumSimplified + artists: SpotifyArtistSimplified[] + external_urls: SpotifyExternalUrls + uri: string +} + +export interface SpotifyArtist { + id: string + name: string + genres: string[] + popularity: number + followers: { total: number } + images: SpotifyImage[] + external_urls: SpotifyExternalUrls +} + +export interface SpotifyAlbum { + id: string + name: string + album_type: string + total_tracks: number + release_date: string + release_date_precision: string + label: string + popularity: number + genres: string[] + images: SpotifyImage[] + artists: SpotifyArtistSimplified[] + tracks: { + items: SpotifyTrack[] + total: number + } + external_urls: SpotifyExternalUrls +} + +export interface SpotifyPlaylist { + id: string + name: string + description: string | null + public: boolean + collaborative: boolean + owner: { + id: string + display_name: string + } + images: SpotifyImage[] + tracks: { + total: number + } + external_urls: SpotifyExternalUrls + snapshot_id: string +} + +export interface SpotifyPlaylistTrack { + added_at: string + added_by: { + id: string + } + track: SpotifyTrack +} + +export interface SpotifyUser { + id: string + display_name: string + email?: string + country?: string + product?: string + followers: { total: number } + images: SpotifyImage[] + external_urls: SpotifyExternalUrls +} + +export interface SpotifyDevice { + id: string + is_active: boolean + is_private_session: boolean + is_restricted: boolean + name: string + type: string + volume_percent: number +} + +export interface SpotifyPlaybackState { + device: SpotifyDevice + shuffle_state: boolean + repeat_state: string + timestamp: number + progress_ms: number + is_playing: boolean + item: SpotifyTrack | null + currently_playing_type: string +} + +/** + * Search + */ +export interface SpotifySearchParams extends SpotifyBaseParams { + query: string + type?: string + limit?: number + offset?: number + market?: string +} + +export interface SpotifySearchResponse extends ToolResponse { + output: { + tracks: Array<{ + id: string + name: string + artists: string[] + album: string + duration_ms: number + popularity: number + preview_url: string | null + external_url: string + }> + artists: Array<{ + id: string + name: string + genres: string[] + popularity: number + followers: number + image_url: string | null + external_url: string + }> + albums: Array<{ + id: string + name: string + artists: string[] + total_tracks: number + release_date: string + image_url: string | null + external_url: string + }> + playlists: Array<{ + id: string + name: string + description: string | null + owner: string + total_tracks: number + image_url: string | null + external_url: string + }> + } +} + +/** + * Get Track + */ +export interface SpotifyGetTrackParams extends SpotifyBaseParams { + trackId: string + market?: string +} + +export interface SpotifyGetTrackResponse extends ToolResponse { + output: { + id: string + name: string + artists: Array<{ id: string; name: string }> + album: { + id: string + name: string + image_url: string | null + } + duration_ms: number + explicit: boolean + popularity: number + preview_url: string | null + external_url: string + uri: string + } +} + +/** + * Get Multiple Tracks + */ +export interface SpotifyGetTracksParams extends SpotifyBaseParams { + trackIds: string + market?: string +} + +export interface SpotifyGetTracksResponse extends ToolResponse { + output: { + tracks: Array<{ + id: string + name: string + artists: Array<{ id: string; name: string }> + album: { + id: string + name: string + image_url: string | null + } + duration_ms: number + explicit: boolean + popularity: number + preview_url: string | null + external_url: string + }> + } +} + +/** + * Get Album + */ +export interface SpotifyGetAlbumParams extends SpotifyBaseParams { + albumId: string + market?: string +} + +export interface SpotifyGetAlbumResponse extends ToolResponse { + output: { + id: string + name: string + artists: Array<{ id: string; name: string }> + album_type: string + total_tracks: number + release_date: string + label: string + popularity: number + genres: string[] + image_url: string | null + tracks: Array<{ + id: string + name: string + duration_ms: number + track_number: number + }> + external_url: string + } +} + +/** + * Get Album Tracks + */ +export interface SpotifyGetAlbumTracksParams extends SpotifyBaseParams { + albumId: string + limit?: number + offset?: number + market?: string +} + +export 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 + } +} + +/** + * Get Artist + */ +export interface SpotifyGetArtistParams extends SpotifyBaseParams { + artistId: string +} + +export interface SpotifyGetArtistResponse extends ToolResponse { + output: { + id: string + name: string + genres: string[] + popularity: number + followers: number + image_url: string | null + external_url: string + } +} + +/** + * Get Artist Albums + */ +export interface SpotifyGetArtistAlbumsParams extends SpotifyBaseParams { + artistId: string + include_groups?: string + limit?: number + offset?: number + market?: string +} + +export interface SpotifyGetArtistAlbumsResponse extends ToolResponse { + output: { + albums: Array<{ + id: string + name: string + album_type: string + total_tracks: number + release_date: string + image_url: string | null + external_url: string + }> + total: number + next: string | null + } +} + +/** + * Get Artist Top Tracks + */ +export interface SpotifyGetArtistTopTracksParams extends SpotifyBaseParams { + artistId: string + market?: string +} + +export interface SpotifyGetArtistTopTracksResponse extends ToolResponse { + output: { + tracks: Array<{ + id: string + name: string + album: { + id: string + name: string + image_url: string | null + } + duration_ms: number + popularity: number + preview_url: string | null + external_url: string + }> + } +} + +/** + * Get Playlist + */ +export interface SpotifyGetPlaylistParams extends SpotifyBaseParams { + playlistId: string + market?: string +} + +export interface SpotifyGetPlaylistResponse extends ToolResponse { + output: { + id: string + name: string + description: string | null + public: boolean + collaborative: boolean + owner: { + id: string + display_name: string + } + image_url: string | null + total_tracks: number + snapshot_id: string + external_url: string + } +} + +/** + * Get Playlist Tracks + */ +export interface SpotifyGetPlaylistTracksParams extends SpotifyBaseParams { + playlistId: string + limit?: number + offset?: number + market?: string +} + +export interface SpotifyGetPlaylistTracksResponse extends ToolResponse { + output: { + tracks: Array<{ + added_at: string + added_by: string + track: { + id: string + name: string + artists: Array<{ id: string; name: string }> + album: { + id: string + name: string + image_url: string | null + } + duration_ms: number + popularity: number + external_url: string + } + }> + total: number + next: string | null + } +} + +/** + * Get User Playlists + */ +export interface SpotifyGetUserPlaylistsParams extends SpotifyBaseParams { + limit?: number + offset?: number +} + +export interface SpotifyGetUserPlaylistsResponse extends ToolResponse { + output: { + playlists: Array<{ + id: string + name: string + description: string | null + public: boolean + collaborative: boolean + owner: string + total_tracks: number + image_url: string | null + external_url: string + }> + total: number + next: string | null + } +} + +/** + * Create Playlist + */ +export interface SpotifyCreatePlaylistParams extends SpotifyBaseParams { + name: string + description?: string + public?: boolean + collaborative?: boolean +} + +export interface SpotifyCreatePlaylistResponse extends ToolResponse { + output: { + id: string + name: string + description: string | null + public: boolean + collaborative: boolean + snapshot_id: string + external_url: string + } +} + +/** + * Add Tracks to Playlist + */ +export interface SpotifyAddTracksToPlaylistParams extends SpotifyBaseParams { + playlistId: string + uris: string + position?: number +} + +export interface SpotifyAddTracksToPlaylistResponse extends ToolResponse { + output: { + snapshot_id: string + } +} + +/** + * Remove Tracks from Playlist + */ +export interface SpotifyRemoveTracksFromPlaylistParams extends SpotifyBaseParams { + playlistId: string + uris: string +} + +export interface SpotifyRemoveTracksFromPlaylistResponse extends ToolResponse { + output: { + snapshot_id: string + } +} + +/** + * Update Playlist + */ +export interface SpotifyUpdatePlaylistParams extends SpotifyBaseParams { + playlistId: string + name?: string + description?: string + public?: boolean + collaborative?: boolean +} + +export interface SpotifyUpdatePlaylistResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Get Current User + */ +export interface SpotifyGetCurrentUserParams extends SpotifyBaseParams {} + +export interface SpotifyGetCurrentUserResponse extends ToolResponse { + output: { + id: string + display_name: string + email: string | null + country: string | null + product: string | null + followers: number + image_url: string | null + external_url: string + } +} + +/** + * Get User Profile + */ +export interface SpotifyGetUserProfileParams extends SpotifyBaseParams { + userId: string +} + +export interface SpotifyGetUserProfileResponse extends ToolResponse { + output: { + id: string + display_name: string + followers: number + image_url: string | null + external_url: string + } +} + +/** + * Get Top Items (Tracks or Artists) + */ +export interface SpotifyGetTopItemsParams extends SpotifyBaseParams { + type: 'tracks' | 'artists' + time_range?: 'short_term' | 'medium_term' | 'long_term' + limit?: number + offset?: number +} + +export interface SpotifyGetTopTracksResponse extends ToolResponse { + output: { + tracks: Array<{ + id: string + name: string + artists: Array<{ id: string; name: string }> + album: { + id: string + name: string + image_url: string | null + } + duration_ms: number + popularity: number + external_url: string + }> + total: number + next: string | null + } +} + +export interface SpotifyGetTopArtistsResponse 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 + } +} + +/** + * Get Recently Played + */ +export interface SpotifyGetRecentlyPlayedParams extends SpotifyBaseParams { + limit?: number + after?: number + before?: number +} + +export interface SpotifyGetRecentlyPlayedResponse extends ToolResponse { + output: { + items: Array<{ + played_at: string + 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 + } + }> + next: string | null + } +} + +/** + * Get Saved Tracks + */ +export interface SpotifyGetSavedTracksParams extends SpotifyBaseParams { + limit?: number + offset?: number + market?: string +} + +export interface SpotifyGetSavedTracksResponse extends ToolResponse { + output: { + tracks: Array<{ + added_at: string + track: { + id: string + name: string + artists: Array<{ id: string; name: string }> + album: { + id: string + name: string + image_url: string | null + } + duration_ms: number + popularity: number + external_url: string + } + }> + total: number + next: string | null + } +} + +/** + * Save Tracks + */ +export interface SpotifySaveTracksParams extends SpotifyBaseParams { + trackIds: string +} + +export interface SpotifySaveTracksResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Remove Saved Tracks + */ +export interface SpotifyRemoveSavedTracksParams extends SpotifyBaseParams { + trackIds: string +} + +export interface SpotifyRemoveSavedTracksResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Check Saved Tracks + */ +export interface SpotifyCheckSavedTracksParams extends SpotifyBaseParams { + trackIds: string +} + +export interface SpotifyCheckSavedTracksResponse extends ToolResponse { + output: { + results: Array<{ + id: string + saved: boolean + }> + all_saved: boolean + none_saved: boolean + } +} + +/** + * Browse Categories + */ +export interface SpotifyBrowseCategoriesParams extends SpotifyBaseParams { + country?: string + locale?: string + limit?: number + offset?: number +} + +export interface SpotifyBrowseCategoriesResponse extends ToolResponse { + output: { + categories: Array<{ + id: string + name: string + icon_url: string | null + }> + total: number + next: string | null + } +} + +/** + * Browse New Releases + */ +export interface SpotifyBrowseNewReleasesParams extends SpotifyBaseParams { + country?: string + limit?: number + offset?: number +} + +export interface SpotifyBrowseNewReleasesResponse extends ToolResponse { + output: { + albums: Array<{ + id: string + name: string + artists: string[] + album_type: string + total_tracks: number + release_date: string + image_url: string | null + external_url: string + }> + total: number + next: string | null + } +} + +/** + * Player - Get Playback State + */ +export interface SpotifyGetPlaybackStateParams extends SpotifyBaseParams { + market?: string +} + +export interface SpotifyGetPlaybackStateResponse extends ToolResponse { + output: { + is_playing: boolean + device: { + id: string + name: string + type: string + volume_percent: number + } | null + progress_ms: number | null + currently_playing_type: string + shuffle_state: boolean + repeat_state: string + track: { + id: string + name: string + artists: Array<{ id: string; name: string }> + album: { + id: string + name: string + image_url: string | null + } + duration_ms: number + } | null + } +} + +/** + * Player - Get Currently Playing + */ +export interface SpotifyGetCurrentlyPlayingParams extends SpotifyBaseParams { + market?: string +} + +export 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 + } +} + +/** + * Player - Get Devices + */ +export interface SpotifyGetDevicesParams extends SpotifyBaseParams {} + +export interface SpotifyGetDevicesResponse extends ToolResponse { + output: { + devices: Array<{ + id: string + is_active: boolean + is_private_session: boolean + is_restricted: boolean + name: string + type: string + volume_percent: number + }> + } +} + +/** + * Player - Play + */ +export interface SpotifyPlayParams extends SpotifyBaseParams { + device_id?: string + context_uri?: string + uris?: string + offset?: number + position_ms?: number +} + +export interface SpotifyPlayResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Pause + */ +export interface SpotifyPauseParams extends SpotifyBaseParams { + device_id?: string +} + +export interface SpotifyPauseResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Skip Next + */ +export interface SpotifySkipNextParams extends SpotifyBaseParams { + device_id?: string +} + +export interface SpotifySkipNextResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Skip Previous + */ +export interface SpotifySkipPreviousParams extends SpotifyBaseParams { + device_id?: string +} + +export interface SpotifySkipPreviousResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Seek + */ +export interface SpotifySeekParams extends SpotifyBaseParams { + position_ms: number + device_id?: string +} + +export interface SpotifySeekResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Set Volume + */ +export interface SpotifySetVolumeParams extends SpotifyBaseParams { + volume_percent: number + device_id?: string +} + +export interface SpotifySetVolumeResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Add to Queue + */ +export interface SpotifyAddToQueueParams extends SpotifyBaseParams { + uri: string + device_id?: string +} + +export interface SpotifyAddToQueueResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Transfer Playback + */ +export interface SpotifyTransferPlaybackParams extends SpotifyBaseParams { + device_id: string + play?: boolean +} + +export interface SpotifyTransferPlaybackResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Set Repeat + */ +export interface SpotifySetRepeatParams extends SpotifyBaseParams { + state: 'track' | 'context' | 'off' + device_id?: string +} + +export interface SpotifySetRepeatResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Player - Set Shuffle + */ +export interface SpotifySetShuffleParams extends SpotifyBaseParams { + state: boolean + device_id?: string +} + +export interface SpotifySetShuffleResponse extends ToolResponse { + output: { + success: boolean + } +} + +/** + * Get New Releases + */ +export interface SpotifyGetNewReleasesParams extends SpotifyBaseParams { + country?: string + limit?: number + offset?: number +} + +export interface SpotifyGetNewReleasesResponse extends ToolResponse { + output: { + albums: Array<{ + id: string + name: string + artists: Array<{ id: string; name: string }> + release_date: string + total_tracks: number + album_type: string + image_url: string | null + external_url: string + }> + total: number + next: string | null + } +} + +/** + * Get Categories + */ +export interface SpotifyGetCategoriesParams extends SpotifyBaseParams { + country?: string + locale?: string + limit?: number +} + +export interface SpotifyGetCategoriesResponse extends ToolResponse { + output: { + categories: Array<{ + id: string + name: string + icon_url: string | null + }> + total: number + } +} diff --git a/apps/sim/tools/spotify/unfollow_artists.ts b/apps/sim/tools/spotify/unfollow_artists.ts new file mode 100644 index 000000000..300bc0499 --- /dev/null +++ b/apps/sim/tools/spotify/unfollow_artists.ts @@ -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 => { + return { + success: true, + output: { success: true }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether artists were unfollowed successfully' }, + }, +} diff --git a/apps/sim/tools/spotify/unfollow_playlist.ts b/apps/sim/tools/spotify/unfollow_playlist.ts new file mode 100644 index 000000000..b1d40d017 --- /dev/null +++ b/apps/sim/tools/spotify/unfollow_playlist.ts @@ -0,0 +1,50 @@ +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, + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether unfollow succeeded' }, + }, +} diff --git a/apps/sim/tools/spotify/update_playlist.ts b/apps/sim/tools/spotify/update_playlist.ts new file mode 100644 index 000000000..b654981b4 --- /dev/null +++ b/apps/sim/tools/spotify/update_playlist.ts @@ -0,0 +1,76 @@ +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, + description: 'The Spotify playlist ID', + }, + name: { + type: 'string', + required: false, + description: 'New name for the playlist', + }, + description: { + type: 'string', + required: false, + description: 'New description for the playlist', + }, + public: { + type: 'boolean', + required: false, + 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 = {} + 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 => { + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether update succeeded' }, + }, +}