mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
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 <walif6@gmail.com>
This commit is contained in:
committed by
GitHub
parent
132f4bca38
commit
ecf5209e6f
@@ -4203,3 +4203,15 @@ export function RssIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 496 512' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='#1ed760'
|
||||
d='M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8Z'
|
||||
/>
|
||||
<path d='M406.6 231.1c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3zm-31 76.2c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm-26.9 65.6c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
ShopifyIcon,
|
||||
SlackIcon,
|
||||
SmtpIcon,
|
||||
SpotifyIcon,
|
||||
SQSIcon,
|
||||
SshIcon,
|
||||
STTIcon,
|
||||
@@ -118,115 +119,116 @@ import {
|
||||
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"shopify",
|
||||
"slack",
|
||||
"smtp",
|
||||
"spotify",
|
||||
"sqs",
|
||||
"ssh",
|
||||
"stagehand",
|
||||
|
||||
1456
apps/docs/content/docs/en/tools/spotify.mdx
Normal file
1456
apps/docs/content/docs/en/tools/spotify.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 |
|
||||
|
||||
|
||||
|
||||
|
||||
1355
apps/sim/blocks/blocks/spotify.ts
Normal file
1355
apps/sim/blocks/blocks/spotify.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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<string, BlockConfig> = {
|
||||
sharepoint: SharepointBlock,
|
||||
shopify: ShopifyBlock,
|
||||
slack: SlackBlock,
|
||||
spotify: SpotifyBlock,
|
||||
smtp: SmtpBlock,
|
||||
sftp: SftpBlock,
|
||||
ssh: SSHBlock,
|
||||
|
||||
@@ -4203,3 +4203,15 @@ export function RssIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 496 512' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='#1ed760'
|
||||
d='M248 8C111.1 8 0 119.1 0 256s111.1 248 248 248 248-111.1 248-248S384.9 8 248 8Z'
|
||||
/>
|
||||
<path d='M406.6 231.1c-5.2 0-8.4-1.3-12.9-3.9-71.2-42.5-198.5-52.7-280.9-29.7-3.6 1-8.1 2.6-12.9 2.6-13.2 0-23.3-10.3-23.3-23.6 0-13.6 8.4-21.3 17.4-23.9 35.2-10.3 74.6-15.2 117.5-15.2 73 0 149.5 15.2 205.4 47.8 7.8 4.5 12.9 10.7 12.9 22.6 0 13.6-11 23.3-23.2 23.3zm-31 76.2c-5.2 0-8.7-2.3-12.3-4.2-62.5-37-155.7-51.9-238.6-29.4-4.8 1.3-7.4 2.6-11.9 2.6-10.7 0-19.4-8.7-19.4-19.4s5.2-17.8 15.5-20.7c27.8-7.8 56.2-13.6 97.8-13.6 64.9 0 127.6 16.1 177 45.5 8.1 4.8 11.3 11 11.3 19.7-.1 10.8-8.5 19.5-19.4 19.5zm-26.9 65.6c-4.2 0-6.8-1.3-10.7-3.6-62.4-37.6-135-39.2-206.7-24.5-3.9 1-9 2.6-11.9 2.6-9.7 0-15.8-7.7-15.8-15.8 0-10.3 6.1-15.2 13.6-16.8 81.9-18.1 165.6-16.5 237 26.2 6.1 3.9 9.7 7.4 9.7 16.5s-7.1 15.4-15.2 15.4z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, OAuthProviderConfig> = {
|
||||
},
|
||||
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}`)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, ToolConfig> = {
|
||||
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,
|
||||
}
|
||||
|
||||
58
apps/sim/tools/spotify/add_playlist_cover.ts
Normal file
58
apps/sim/tools/spotify/add_playlist_cover.ts
Normal file
@@ -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<SpotifyAddPlaylistCoverResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether upload succeeded' },
|
||||
},
|
||||
}
|
||||
59
apps/sim/tools/spotify/add_to_queue.ts
Normal file
59
apps/sim/tools/spotify/add_to_queue.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyAddToQueueParams, SpotifyAddToQueueResponse } from './types'
|
||||
|
||||
export const spotifyAddToQueueTool: ToolConfig<SpotifyAddToQueueParams, SpotifyAddToQueueResponse> =
|
||||
{
|
||||
id: 'spotify_add_to_queue',
|
||||
name: 'Spotify Add to Queue',
|
||||
description: "Add a track to the user's playback queue.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Spotify URI of the track to add (e.g., "spotify:track:xxx")',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID. If not provided, uses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = `https://api.spotify.com/v1/me/player/queue?uri=${encodeURIComponent(params.uri)}`
|
||||
if (params.device_id) {
|
||||
url += `&device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyAddToQueueResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether track was added to queue' },
|
||||
},
|
||||
}
|
||||
71
apps/sim/tools/spotify/add_tracks_to_playlist.ts
Normal file
71
apps/sim/tools/spotify/add_tracks_to_playlist.ts
Normal file
@@ -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<SpotifyAddTracksToPlaylistResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
snapshot_id: data.snapshot_id,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID after modification' },
|
||||
},
|
||||
}
|
||||
64
apps/sim/tools/spotify/check_following.ts
Normal file
64
apps/sim/tools/spotify/check_following.ts
Normal file
@@ -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<SpotifyCheckFollowingResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each ID' },
|
||||
},
|
||||
}
|
||||
64
apps/sim/tools/spotify/check_playlist_followers.ts
Normal file
64
apps/sim/tools/spotify/check_playlist_followers.ts
Normal file
@@ -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<SpotifyCheckPlaylistFollowersResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each user' },
|
||||
},
|
||||
}
|
||||
58
apps/sim/tools/spotify/check_saved_albums.ts
Normal file
58
apps/sim/tools/spotify/check_saved_albums.ts
Normal file
@@ -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<SpotifyCheckSavedAlbumsResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each album' },
|
||||
},
|
||||
}
|
||||
58
apps/sim/tools/spotify/check_saved_audiobooks.ts
Normal file
58
apps/sim/tools/spotify/check_saved_audiobooks.ts
Normal file
@@ -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<SpotifyCheckSavedAudiobooksResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each audiobook' },
|
||||
},
|
||||
}
|
||||
58
apps/sim/tools/spotify/check_saved_episodes.ts
Normal file
58
apps/sim/tools/spotify/check_saved_episodes.ts
Normal file
@@ -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<SpotifyCheckSavedEpisodesResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each episode' },
|
||||
},
|
||||
}
|
||||
58
apps/sim/tools/spotify/check_saved_shows.ts
Normal file
58
apps/sim/tools/spotify/check_saved_shows.ts
Normal file
@@ -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<SpotifyCheckSavedShowsResponse> => {
|
||||
const results = await response.json()
|
||||
return { success: true, output: { results } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of booleans for each show' },
|
||||
},
|
||||
}
|
||||
71
apps/sim/tools/spotify/check_saved_tracks.ts
Normal file
71
apps/sim/tools/spotify/check_saved_tracks.ts
Normal file
@@ -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<SpotifyCheckSavedTracksResponse> => {
|
||||
const data = await response.json()
|
||||
const ids = (params?.trackIds || '')
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
|
||||
const results = ids.map((id, index) => ({
|
||||
id,
|
||||
saved: data[index] || false,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
all_saved: data.every((saved: boolean) => saved),
|
||||
none_saved: data.every((saved: boolean) => !saved),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'json', description: 'Array of track IDs with saved status' },
|
||||
all_saved: { type: 'boolean', description: 'Whether all tracks are saved' },
|
||||
none_saved: { type: 'boolean', description: 'Whether no tracks are saved' },
|
||||
},
|
||||
}
|
||||
89
apps/sim/tools/spotify/create_playlist.ts
Normal file
89
apps/sim/tools/spotify/create_playlist.ts
Normal file
@@ -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<SpotifyCreatePlaylistResponse> => {
|
||||
const playlist = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
public: playlist.public,
|
||||
collaborative: playlist.collaborative,
|
||||
snapshot_id: playlist.snapshot_id,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify playlist ID' },
|
||||
name: { type: 'string', description: 'Playlist name' },
|
||||
description: { type: 'string', description: 'Playlist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether the playlist is public' },
|
||||
collaborative: { type: 'boolean', description: 'Whether collaborative' },
|
||||
snapshot_id: { type: 'string', description: 'Playlist snapshot ID' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
64
apps/sim/tools/spotify/follow_artists.ts
Normal file
64
apps/sim/tools/spotify/follow_artists.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyFollowArtistsParams {
|
||||
accessToken: string
|
||||
artistIds: string
|
||||
}
|
||||
|
||||
interface SpotifyFollowArtistsResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyFollowArtistsTool: ToolConfig<
|
||||
SpotifyFollowArtistsParams,
|
||||
SpotifyFollowArtistsResponse
|
||||
> = {
|
||||
id: 'spotify_follow_artists',
|
||||
name: 'Spotify Follow Artists',
|
||||
description: 'Follow one or more artists.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-follow-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
artistIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated artist IDs to follow (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.artistIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/following?type=artist&ids=${ids}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyFollowArtistsResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether artists were followed successfully' },
|
||||
},
|
||||
}
|
||||
61
apps/sim/tools/spotify/follow_playlist.ts
Normal file
61
apps/sim/tools/spotify/follow_playlist.ts
Normal file
@@ -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<SpotifyFollowPlaylistResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether follow succeeded' },
|
||||
},
|
||||
}
|
||||
87
apps/sim/tools/spotify/get_album.ts
Normal file
87
apps/sim/tools/spotify/get_album.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyGetAlbumParams, SpotifyGetAlbumResponse } from './types'
|
||||
|
||||
export const spotifyGetAlbumTool: ToolConfig<SpotifyGetAlbumParams, SpotifyGetAlbumResponse> = {
|
||||
id: 'spotify_get_album',
|
||||
name: 'Spotify Get Album',
|
||||
description:
|
||||
'Get detailed information about an album on Spotify by its ID, including track listing.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
albumId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the album',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifyGetAlbumResponse> => {
|
||||
const album = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album_type: album.album_type,
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
label: album.label || '',
|
||||
popularity: album.popularity,
|
||||
genres: album.genres || [],
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
tracks: (album.tracks?.items || []).map((t: any) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
duration_ms: t.duration_ms,
|
||||
track_number: t.track_number,
|
||||
})),
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify album ID' },
|
||||
name: { type: 'string', description: 'Album name' },
|
||||
artists: { type: 'array', description: 'List of artists' },
|
||||
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' },
|
||||
},
|
||||
}
|
||||
112
apps/sim/tools/spotify/get_album_tracks.ts
Normal file
112
apps/sim/tools/spotify/get_album_tracks.ts
Normal file
@@ -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<SpotifyGetAlbumTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
duration_ms: track.duration_ms,
|
||||
track_number: track.track_number,
|
||||
disc_number: track.disc_number,
|
||||
explicit: track.explicit || false,
|
||||
preview_url: track.preview_url || null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: { type: 'json', description: 'List of tracks' },
|
||||
total: { type: 'number', description: 'Total number of tracks' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
94
apps/sim/tools/spotify/get_albums.ts
Normal file
94
apps/sim/tools/spotify/get_albums.ts
Normal file
@@ -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<SpotifyGetAlbumsParams, SpotifyGetAlbumsResponse> = {
|
||||
id: 'spotify_get_albums',
|
||||
name: 'Spotify Get Multiple Albums',
|
||||
description: 'Get details for multiple albums by their IDs.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
albumIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated album IDs (max 20)',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifyGetAlbumsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const albums = (data.albums || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album_type: album.album_type,
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { albums },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: { type: 'json', description: 'List of albums' },
|
||||
},
|
||||
}
|
||||
59
apps/sim/tools/spotify/get_artist.ts
Normal file
59
apps/sim/tools/spotify/get_artist.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyGetArtistParams, SpotifyGetArtistResponse } from './types'
|
||||
|
||||
export const spotifyGetArtistTool: ToolConfig<SpotifyGetArtistParams, SpotifyGetArtistResponse> = {
|
||||
id: 'spotify_get_artist',
|
||||
name: 'Spotify Get Artist',
|
||||
description: 'Get detailed information about an artist on Spotify by their ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
artistId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the artist',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.spotify.com/v1/artists/${params.artistId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetArtistResponse> => {
|
||||
const artist = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify artist ID' },
|
||||
name: { type: 'string', description: 'Artist name' },
|
||||
genres: { type: 'array', description: 'List of genres associated with the artist' },
|
||||
popularity: { type: 'number', description: 'Popularity score (0-100)' },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Artist image URL', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
116
apps/sim/tools/spotify/get_artist_albums.ts
Normal file
116
apps/sim/tools/spotify/get_artist_albums.ts
Normal file
@@ -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<SpotifyGetArtistAlbumsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const albums = (data.items || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
album_type: album.album_type,
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
albums,
|
||||
total: data.total || albums.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: {
|
||||
type: 'array',
|
||||
description: "Artist's albums",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify album ID' },
|
||||
name: { type: 'string', description: 'Album name' },
|
||||
album_type: { type: 'string', description: 'Type (album, single, compilation)' },
|
||||
total_tracks: { type: 'number', description: 'Number of tracks' },
|
||||
release_date: { type: 'string', description: 'Release date' },
|
||||
image_url: { type: 'string', description: 'Album cover URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of albums available' },
|
||||
next: { type: 'string', description: 'URL for next page of results', optional: true },
|
||||
},
|
||||
}
|
||||
89
apps/sim/tools/spotify/get_artist_top_tracks.ts
Normal file
89
apps/sim/tools/spotify/get_artist_top_tracks.ts
Normal file
@@ -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<SpotifyGetArtistTopTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.tracks || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: "Artist's top tracks",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
82
apps/sim/tools/spotify/get_artists.ts
Normal file
82
apps/sim/tools/spotify/get_artists.ts
Normal file
@@ -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<SpotifyGetArtistsParams, SpotifyGetArtistsResponse> =
|
||||
{
|
||||
id: 'spotify_get_artists',
|
||||
name: 'Spotify Get Multiple Artists',
|
||||
description: 'Get details for multiple artists by their IDs.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {
|
||||
artistIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated artist IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.artistIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/artists?ids=${ids}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetArtistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const artists = (data.artists || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity || 0,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { artists },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
artists: { type: 'json', description: 'List of artists' },
|
||||
},
|
||||
}
|
||||
95
apps/sim/tools/spotify/get_audiobook.ts
Normal file
95
apps/sim/tools/spotify/get_audiobook.ts
Normal file
@@ -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<SpotifyGetAudiobookResponse> => {
|
||||
const book = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: book.id,
|
||||
name: book.name,
|
||||
authors: book.authors || [],
|
||||
narrators: book.narrators || [],
|
||||
publisher: book.publisher || '',
|
||||
description: book.description || '',
|
||||
total_chapters: book.total_chapters || 0,
|
||||
languages: book.languages || [],
|
||||
image_url: book.images?.[0]?.url || null,
|
||||
external_url: book.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Audiobook ID' },
|
||||
name: { type: 'string', description: 'Audiobook name' },
|
||||
authors: { type: 'json', description: 'Authors' },
|
||||
narrators: { type: 'json', description: 'Narrators' },
|
||||
publisher: { type: 'string', description: 'Publisher' },
|
||||
description: { type: 'string', description: 'Description' },
|
||||
total_chapters: { type: 'number', description: 'Total chapters' },
|
||||
languages: { type: 'json', description: 'Languages' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
104
apps/sim/tools/spotify/get_audiobook_chapters.ts
Normal file
104
apps/sim/tools/spotify/get_audiobook_chapters.ts
Normal file
@@ -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<SpotifyGetAudiobookChaptersResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
chapters: (data.items || []).map((ch: any) => ({
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
chapter_number: ch.chapter_number || 0,
|
||||
duration_ms: ch.duration_ms || 0,
|
||||
image_url: ch.images?.[0]?.url || null,
|
||||
external_url: ch.external_urls?.spotify || '',
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
chapters: { type: 'json', description: 'List of chapters' },
|
||||
total: { type: 'number', description: 'Total chapters' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
87
apps/sim/tools/spotify/get_audiobooks.ts
Normal file
87
apps/sim/tools/spotify/get_audiobooks.ts
Normal file
@@ -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<SpotifyGetAudiobooksResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
audiobooks: (data.audiobooks || []).map((book: any) => ({
|
||||
id: book.id,
|
||||
name: book.name,
|
||||
authors: book.authors || [],
|
||||
total_chapters: book.total_chapters || 0,
|
||||
image_url: book.images?.[0]?.url || null,
|
||||
external_url: book.external_urls?.spotify || '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
audiobooks: { type: 'json', description: 'List of audiobooks' },
|
||||
},
|
||||
}
|
||||
82
apps/sim/tools/spotify/get_categories.ts
Normal file
82
apps/sim/tools/spotify/get_categories.ts
Normal file
@@ -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<SpotifyGetCategoriesResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const categories = (data.categories?.items || []).map((category: any) => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
icon_url: category.icons?.[0]?.url || null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
categories,
|
||||
total: data.categories?.total || 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
categories: { type: 'json', description: 'List of browse categories' },
|
||||
total: { type: 'number', description: 'Total number of categories' },
|
||||
},
|
||||
}
|
||||
58
apps/sim/tools/spotify/get_current_user.ts
Normal file
58
apps/sim/tools/spotify/get_current_user.ts
Normal file
@@ -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<SpotifyGetCurrentUserResponse> => {
|
||||
const user = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: user.id,
|
||||
display_name: user.display_name || '',
|
||||
email: user.email || null,
|
||||
country: user.country || null,
|
||||
product: user.product || null,
|
||||
followers: user.followers?.total || 0,
|
||||
image_url: user.images?.[0]?.url || null,
|
||||
external_url: user.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify user ID' },
|
||||
display_name: { type: 'string', description: 'Display name' },
|
||||
email: { type: 'string', description: 'Email address', optional: true },
|
||||
country: { type: 'string', description: 'Country code', optional: true },
|
||||
product: { type: 'string', description: 'Subscription level (free, premium)', optional: true },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Profile image URL', optional: true },
|
||||
external_url: { type: 'string', description: 'Spotify profile URL' },
|
||||
},
|
||||
}
|
||||
108
apps/sim/tools/spotify/get_currently_playing.ts
Normal file
108
apps/sim/tools/spotify/get_currently_playing.ts
Normal file
@@ -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<SpotifyGetCurrentlyPlayingResponse> => {
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: false,
|
||||
progress_ms: null,
|
||||
track: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: data.is_playing || false,
|
||||
progress_ms: data.progress_ms || null,
|
||||
track: data.item
|
||||
? {
|
||||
id: data.item.id,
|
||||
name: data.item.name,
|
||||
artists: data.item.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: data.item.album?.id || '',
|
||||
name: data.item.album?.name || '',
|
||||
image_url: data.item.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: data.item.duration_ms,
|
||||
external_url: data.item.external_urls?.spotify || '',
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
is_playing: { type: 'boolean', description: 'Whether playback is active' },
|
||||
progress_ms: { type: 'number', description: 'Current position in track (ms)', optional: true },
|
||||
track: { type: 'json', description: 'Currently playing track', optional: true },
|
||||
},
|
||||
}
|
||||
67
apps/sim/tools/spotify/get_devices.ts
Normal file
67
apps/sim/tools/spotify/get_devices.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyGetDevicesParams, SpotifyGetDevicesResponse } from './types'
|
||||
|
||||
export const spotifyGetDevicesTool: ToolConfig<SpotifyGetDevicesParams, SpotifyGetDevicesResponse> =
|
||||
{
|
||||
id: 'spotify_get_devices',
|
||||
name: 'Spotify Get Devices',
|
||||
description: "Get the user's available Spotify playback devices.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-state'],
|
||||
},
|
||||
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/player/devices',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetDevicesResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const devices = (data.devices || []).map((device: any) => ({
|
||||
id: device.id,
|
||||
is_active: device.is_active,
|
||||
is_private_session: device.is_private_session,
|
||||
is_restricted: device.is_restricted,
|
||||
name: device.name,
|
||||
type: device.type,
|
||||
volume_percent: device.volume_percent,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
devices,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
devices: {
|
||||
type: 'array',
|
||||
description: 'Available playback devices',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Device ID' },
|
||||
is_active: { type: 'boolean', description: 'Whether device is active' },
|
||||
is_private_session: { type: 'boolean', description: 'Whether in private session' },
|
||||
is_restricted: { type: 'boolean', description: 'Whether device is restricted' },
|
||||
name: { type: 'string', description: 'Device name' },
|
||||
type: { type: 'string', description: 'Device type (Computer, Smartphone, etc.)' },
|
||||
volume_percent: { type: 'number', description: 'Current volume (0-100)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
94
apps/sim/tools/spotify/get_episode.ts
Normal file
94
apps/sim/tools/spotify/get_episode.ts
Normal file
@@ -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<SpotifyGetEpisodeParams, SpotifyGetEpisodeResponse> =
|
||||
{
|
||||
id: 'spotify_get_episode',
|
||||
name: 'Spotify Get Episode',
|
||||
description: 'Get details for a podcast episode.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
episodeId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
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<SpotifyGetEpisodeResponse> => {
|
||||
const ep = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: ep.id,
|
||||
name: ep.name,
|
||||
description: ep.description || '',
|
||||
duration_ms: ep.duration_ms || 0,
|
||||
release_date: ep.release_date || '',
|
||||
explicit: ep.explicit || false,
|
||||
show: {
|
||||
id: ep.show?.id || '',
|
||||
name: ep.show?.name || '',
|
||||
publisher: ep.show?.publisher || '',
|
||||
},
|
||||
image_url: ep.images?.[0]?.url || null,
|
||||
external_url: ep.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Episode ID' },
|
||||
name: { type: 'string', description: 'Episode name' },
|
||||
description: { type: 'string', description: 'Episode description' },
|
||||
duration_ms: { type: 'number', description: 'Duration in ms' },
|
||||
release_date: { type: 'string', description: 'Release date' },
|
||||
explicit: { type: 'boolean', description: 'Contains explicit content' },
|
||||
show: { type: 'json', description: 'Parent show info' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
91
apps/sim/tools/spotify/get_episodes.ts
Normal file
91
apps/sim/tools/spotify/get_episodes.ts
Normal file
@@ -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<SpotifyGetEpisodesResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
episodes: (data.episodes || []).map((ep: any) => ({
|
||||
id: ep.id,
|
||||
name: ep.name,
|
||||
description: ep.description || '',
|
||||
duration_ms: ep.duration_ms || 0,
|
||||
release_date: ep.release_date || '',
|
||||
show: { id: ep.show?.id || '', name: ep.show?.name || '' },
|
||||
image_url: ep.images?.[0]?.url || null,
|
||||
external_url: ep.external_urls?.spotify || '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
episodes: { type: 'json', description: 'List of episodes' },
|
||||
},
|
||||
}
|
||||
100
apps/sim/tools/spotify/get_followed_artists.ts
Normal file
100
apps/sim/tools/spotify/get_followed_artists.ts
Normal file
@@ -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<SpotifyGetFollowedArtistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const artists = (data.artists?.items || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity || 0,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
artists,
|
||||
total: data.artists?.total || 0,
|
||||
next: data.artists?.cursors?.after || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
artists: { type: '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 },
|
||||
},
|
||||
}
|
||||
47
apps/sim/tools/spotify/get_markets.ts
Normal file
47
apps/sim/tools/spotify/get_markets.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyGetMarketsParams {
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
interface SpotifyGetMarketsResponse extends ToolResponse {
|
||||
output: {
|
||||
markets: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyGetMarketsTool: ToolConfig<SpotifyGetMarketsParams, SpotifyGetMarketsResponse> =
|
||||
{
|
||||
id: 'spotify_get_markets',
|
||||
name: 'Spotify Get Available Markets',
|
||||
description: 'Get the list of markets where Spotify is available.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-private'],
|
||||
},
|
||||
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/markets',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetMarketsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: { markets: data.markets || [] },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
markets: { type: 'json', description: 'List of ISO country codes' },
|
||||
},
|
||||
}
|
||||
88
apps/sim/tools/spotify/get_new_releases.ts
Normal file
88
apps/sim/tools/spotify/get_new_releases.ts
Normal file
@@ -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<SpotifyGetNewReleasesResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const albums = (data.albums?.items || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
release_date: album.release_date,
|
||||
total_tracks: album.total_tracks,
|
||||
album_type: album.album_type,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
albums,
|
||||
total: data.albums?.total || 0,
|
||||
next: data.albums?.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: { type: '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 },
|
||||
},
|
||||
}
|
||||
103
apps/sim/tools/spotify/get_playback_state.ts
Normal file
103
apps/sim/tools/spotify/get_playback_state.ts
Normal file
@@ -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<SpotifyGetPlaybackStateResponse> => {
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: false,
|
||||
device: null,
|
||||
progress_ms: null,
|
||||
currently_playing_type: 'unknown',
|
||||
shuffle_state: false,
|
||||
repeat_state: 'off',
|
||||
track: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
is_playing: data.is_playing || false,
|
||||
device: data.device
|
||||
? {
|
||||
id: data.device.id,
|
||||
name: data.device.name,
|
||||
type: data.device.type,
|
||||
volume_percent: data.device.volume_percent,
|
||||
}
|
||||
: null,
|
||||
progress_ms: data.progress_ms,
|
||||
currently_playing_type: data.currently_playing_type || 'unknown',
|
||||
shuffle_state: data.shuffle_state || false,
|
||||
repeat_state: data.repeat_state || 'off',
|
||||
track: data.item
|
||||
? {
|
||||
id: data.item.id,
|
||||
name: data.item.name,
|
||||
artists: data.item.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: data.item.album?.id || '',
|
||||
name: data.item.album?.name || '',
|
||||
image_url: data.item.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: data.item.duration_ms,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
is_playing: { type: 'boolean', description: 'Whether playback is active' },
|
||||
device: { type: 'object', description: 'Active device information', optional: true },
|
||||
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 },
|
||||
},
|
||||
}
|
||||
83
apps/sim/tools/spotify/get_playlist.ts
Normal file
83
apps/sim/tools/spotify/get_playlist.ts
Normal file
@@ -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<SpotifyGetPlaylistResponse> => {
|
||||
const playlist = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
public: playlist.public,
|
||||
collaborative: playlist.collaborative,
|
||||
owner: {
|
||||
id: playlist.owner?.id || '',
|
||||
display_name: playlist.owner?.display_name || '',
|
||||
},
|
||||
image_url: playlist.images?.[0]?.url || null,
|
||||
total_tracks: playlist.tracks?.total || 0,
|
||||
snapshot_id: playlist.snapshot_id,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify playlist ID' },
|
||||
name: { type: 'string', description: 'Playlist name' },
|
||||
description: { type: 'string', description: 'Playlist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether the playlist is public' },
|
||||
collaborative: { type: 'boolean', description: 'Whether the playlist is collaborative' },
|
||||
owner: { type: 'object', description: 'Playlist owner information' },
|
||||
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' },
|
||||
},
|
||||
}
|
||||
66
apps/sim/tools/spotify/get_playlist_cover.ts
Normal file
66
apps/sim/tools/spotify/get_playlist_cover.ts
Normal file
@@ -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<SpotifyGetPlaylistCoverResponse> => {
|
||||
const images = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
images: (images || []).map((img: any) => ({
|
||||
url: img.url,
|
||||
width: img.width || null,
|
||||
height: img.height || null,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
images: { type: 'json', description: 'List of cover images' },
|
||||
},
|
||||
}
|
||||
113
apps/sim/tools/spotify/get_playlist_tracks.ts
Normal file
113
apps/sim/tools/spotify/get_playlist_tracks.ts
Normal file
@@ -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<SpotifyGetPlaylistTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || [])
|
||||
.filter((item: any) => item.track !== null)
|
||||
.map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
added_by: item.added_by?.id || '',
|
||||
track: {
|
||||
id: item.track.id,
|
||||
name: item.track.name,
|
||||
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: item.track.album?.id || '',
|
||||
name: item.track.album?.name || '',
|
||||
image_url: item.track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: item.track.duration_ms,
|
||||
popularity: item.track.popularity,
|
||||
external_url: item.track.external_urls?.spotify || '',
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || tracks.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of tracks in the playlist',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
added_at: { type: 'string', description: 'When the track was added' },
|
||||
added_by: { type: 'string', description: 'User ID who added the track' },
|
||||
track: { type: 'object', description: 'Track information' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of tracks in playlist' },
|
||||
next: { type: 'string', description: 'URL for next page of results', optional: true },
|
||||
},
|
||||
}
|
||||
85
apps/sim/tools/spotify/get_queue.ts
Normal file
85
apps/sim/tools/spotify/get_queue.ts
Normal file
@@ -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<SpotifyGetQueueParams, SpotifyGetQueueResponse> = {
|
||||
id: 'spotify_get_queue',
|
||||
name: 'Spotify Get Queue',
|
||||
description: "Get the user's playback queue.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-state'],
|
||||
},
|
||||
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/player/queue',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyGetQueueResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const formatTrack = (track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
currently_playing: data.currently_playing ? formatTrack(data.currently_playing) : null,
|
||||
queue: (data.queue || []).map(formatTrack),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
currently_playing: { type: 'json', description: 'Currently playing track', optional: true },
|
||||
queue: { type: 'json', description: 'Upcoming tracks in queue' },
|
||||
},
|
||||
}
|
||||
102
apps/sim/tools/spotify/get_recently_played.ts
Normal file
102
apps/sim/tools/spotify/get_recently_played.ts
Normal file
@@ -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<SpotifyGetRecentlyPlayedResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const items = (data.items || []).map((item: any) => ({
|
||||
played_at: item.played_at,
|
||||
track: {
|
||||
id: item.track.id,
|
||||
name: item.track.name,
|
||||
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: item.track.album?.id || '',
|
||||
name: item.track.album?.name || '',
|
||||
image_url: item.track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: item.track.duration_ms,
|
||||
external_url: item.track.external_urls?.spotify || '',
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Recently played tracks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
played_at: { type: 'string', description: 'When the track was played' },
|
||||
track: { type: 'object', description: 'Track information' },
|
||||
},
|
||||
},
|
||||
},
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
106
apps/sim/tools/spotify/get_saved_albums.ts
Normal file
106
apps/sim/tools/spotify/get_saved_albums.ts
Normal file
@@ -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<SpotifyGetSavedAlbumsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
albums: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
album: {
|
||||
id: item.album.id,
|
||||
name: item.album.name,
|
||||
artists: item.album.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
total_tracks: item.album.total_tracks,
|
||||
release_date: item.album.release_date,
|
||||
image_url: item.album.images?.[0]?.url || null,
|
||||
external_url: item.album.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
albums: { type: 'json', description: 'List of saved albums' },
|
||||
total: { type: 'number', description: 'Total saved albums' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
97
apps/sim/tools/spotify/get_saved_audiobooks.ts
Normal file
97
apps/sim/tools/spotify/get_saved_audiobooks.ts
Normal file
@@ -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<SpotifyGetSavedAudiobooksResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
audiobooks: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
audiobook: {
|
||||
id: item.audiobook?.id || item.id,
|
||||
name: item.audiobook?.name || item.name,
|
||||
authors: item.audiobook?.authors || item.authors || [],
|
||||
total_chapters: item.audiobook?.total_chapters || item.total_chapters || 0,
|
||||
image_url: item.audiobook?.images?.[0]?.url || item.images?.[0]?.url || null,
|
||||
external_url:
|
||||
item.audiobook?.external_urls?.spotify || item.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
audiobooks: { type: 'json', description: 'List of saved audiobooks' },
|
||||
total: { type: 'number', description: 'Total saved audiobooks' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
106
apps/sim/tools/spotify/get_saved_episodes.ts
Normal file
106
apps/sim/tools/spotify/get_saved_episodes.ts
Normal file
@@ -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<SpotifyGetSavedEpisodesResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
episodes: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
episode: {
|
||||
id: item.episode.id,
|
||||
name: item.episode.name,
|
||||
duration_ms: item.episode.duration_ms || 0,
|
||||
release_date: item.episode.release_date || '',
|
||||
show: { id: item.episode.show?.id || '', name: item.episode.show?.name || '' },
|
||||
image_url: item.episode.images?.[0]?.url || null,
|
||||
external_url: item.episode.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
episodes: { type: 'json', description: 'List of saved episodes' },
|
||||
total: { type: 'number', description: 'Total saved episodes' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
96
apps/sim/tools/spotify/get_saved_shows.ts
Normal file
96
apps/sim/tools/spotify/get_saved_shows.ts
Normal file
@@ -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<SpotifyGetSavedShowsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
shows: (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
show: {
|
||||
id: item.show.id,
|
||||
name: item.show.name,
|
||||
publisher: item.show.publisher || '',
|
||||
total_episodes: item.show.total_episodes || 0,
|
||||
image_url: item.show.images?.[0]?.url || null,
|
||||
external_url: item.show.external_urls?.spotify || '',
|
||||
},
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
shows: { type: 'json', description: 'List of saved shows' },
|
||||
total: { type: 'number', description: 'Total saved shows' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
104
apps/sim/tools/spotify/get_saved_tracks.ts
Normal file
104
apps/sim/tools/spotify/get_saved_tracks.ts
Normal file
@@ -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<SpotifyGetSavedTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || []).map((item: any) => ({
|
||||
added_at: item.added_at,
|
||||
track: {
|
||||
id: item.track.id,
|
||||
name: item.track.name,
|
||||
artists: item.track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: item.track.album?.id || '',
|
||||
name: item.track.album?.name || '',
|
||||
image_url: item.track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: item.track.duration_ms,
|
||||
popularity: item.track.popularity,
|
||||
external_url: item.track.external_urls?.spotify || '',
|
||||
},
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || tracks.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: "User's saved tracks",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
added_at: { type: 'string', description: 'When the track was saved' },
|
||||
track: { type: 'object', description: 'Track information' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of saved tracks' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
89
apps/sim/tools/spotify/get_show.ts
Normal file
89
apps/sim/tools/spotify/get_show.ts
Normal file
@@ -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<SpotifyGetShowParams, SpotifyGetShowResponse> = {
|
||||
id: 'spotify_get_show',
|
||||
name: 'Spotify Get Show',
|
||||
description: 'Get details for a podcast show.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
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<SpotifyGetShowResponse> => {
|
||||
const show = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: show.id,
|
||||
name: show.name,
|
||||
description: show.description || '',
|
||||
publisher: show.publisher || '',
|
||||
total_episodes: show.total_episodes || 0,
|
||||
explicit: show.explicit || false,
|
||||
languages: show.languages || [],
|
||||
image_url: show.images?.[0]?.url || null,
|
||||
external_url: show.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Show ID' },
|
||||
name: { type: 'string', description: 'Show name' },
|
||||
description: { type: 'string', description: 'Show description' },
|
||||
publisher: { type: 'string', description: 'Publisher name' },
|
||||
total_episodes: { type: 'number', description: 'Total episodes' },
|
||||
explicit: { type: 'boolean', description: 'Contains explicit content' },
|
||||
languages: { type: 'json', description: 'Languages' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
106
apps/sim/tools/spotify/get_show_episodes.ts
Normal file
106
apps/sim/tools/spotify/get_show_episodes.ts
Normal file
@@ -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<SpotifyGetShowEpisodesResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
episodes: (data.items || []).map((ep: any) => ({
|
||||
id: ep.id,
|
||||
name: ep.name,
|
||||
description: ep.description || '',
|
||||
duration_ms: ep.duration_ms || 0,
|
||||
release_date: ep.release_date || '',
|
||||
image_url: ep.images?.[0]?.url || null,
|
||||
external_url: ep.external_urls?.spotify || '',
|
||||
})),
|
||||
total: data.total || 0,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
episodes: { type: 'json', description: 'List of episodes' },
|
||||
total: { type: 'number', description: 'Total episodes' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
84
apps/sim/tools/spotify/get_shows.ts
Normal file
84
apps/sim/tools/spotify/get_shows.ts
Normal file
@@ -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<SpotifyGetShowsParams, SpotifyGetShowsResponse> = {
|
||||
id: 'spotify_get_shows',
|
||||
name: 'Spotify Get Multiple Shows',
|
||||
description: 'Get details for multiple podcast shows.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-read-playback-position'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
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<SpotifyGetShowsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
shows: (data.shows || []).map((show: any) => ({
|
||||
id: show.id,
|
||||
name: show.name,
|
||||
publisher: show.publisher || '',
|
||||
total_episodes: show.total_episodes || 0,
|
||||
image_url: show.images?.[0]?.url || null,
|
||||
external_url: show.external_urls?.spotify || '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
shows: { type: 'json', description: 'List of shows' },
|
||||
},
|
||||
}
|
||||
100
apps/sim/tools/spotify/get_top_artists.ts
Normal file
100
apps/sim/tools/spotify/get_top_artists.ts
Normal file
@@ -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<SpotifyGetTopArtistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const artists = (data.items || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
artists,
|
||||
total: data.total || artists.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
artists: {
|
||||
type: 'array',
|
||||
description: "User's top artists",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify artist ID' },
|
||||
name: { type: 'string', description: 'Artist name' },
|
||||
genres: { type: 'array', description: 'List of genres' },
|
||||
popularity: { type: 'number', description: 'Popularity score' },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Artist image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of top artists' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
104
apps/sim/tools/spotify/get_top_tracks.ts
Normal file
104
apps/sim/tools/spotify/get_top_tracks.ts
Normal file
@@ -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<SpotifyGetTopTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.items || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
popularity: track.popularity,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
total: data.total || tracks.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: "User's top tracks",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify track ID' },
|
||||
name: { type: 'string', description: 'Track name' },
|
||||
artists: { type: 'array', description: 'List of artists' },
|
||||
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 },
|
||||
},
|
||||
}
|
||||
81
apps/sim/tools/spotify/get_track.ts
Normal file
81
apps/sim/tools/spotify/get_track.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyGetTrackParams, SpotifyGetTrackResponse } from './types'
|
||||
|
||||
export const spotifyGetTrackTool: ToolConfig<SpotifyGetTrackParams, SpotifyGetTrackResponse> = {
|
||||
id: 'spotify_get_track',
|
||||
name: 'Spotify Get Track',
|
||||
description: 'Get detailed information about a specific track on Spotify by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
trackId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Spotify ID of the track',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifyGetTrackResponse> => {
|
||||
const track = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
explicit: track.explicit,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
uri: track.uri,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Spotify track ID' },
|
||||
name: { type: 'string', description: 'Track name' },
|
||||
artists: { type: 'array', description: 'List of artists' },
|
||||
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' },
|
||||
},
|
||||
}
|
||||
94
apps/sim/tools/spotify/get_tracks.ts
Normal file
94
apps/sim/tools/spotify/get_tracks.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyGetTracksParams, SpotifyGetTracksResponse } from './types'
|
||||
|
||||
export const spotifyGetTracksTool: ToolConfig<SpotifyGetTracksParams, SpotifyGetTracksResponse> = {
|
||||
id: 'spotify_get_tracks',
|
||||
name: 'Spotify Get Multiple Tracks',
|
||||
description: 'Get detailed information about multiple tracks on Spotify by their IDs (up to 50).',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
trackIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of Spotify track IDs (max 50)',
|
||||
},
|
||||
market: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifyGetTracksResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.tracks || [])
|
||||
.filter((t: any) => t !== null)
|
||||
.map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => ({ id: a.id, name: a.name })) || [],
|
||||
album: {
|
||||
id: track.album?.id || '',
|
||||
name: track.album?.name || '',
|
||||
image_url: track.album?.images?.[0]?.url || null,
|
||||
},
|
||||
duration_ms: track.duration_ms,
|
||||
explicit: track.explicit,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of tracks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify track ID' },
|
||||
name: { type: 'string', description: 'Track name' },
|
||||
artists: { type: 'array', description: 'List of artists' },
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
96
apps/sim/tools/spotify/get_user_playlists.ts
Normal file
96
apps/sim/tools/spotify/get_user_playlists.ts
Normal file
@@ -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<SpotifyGetUserPlaylistsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const playlists = (data.items || []).map((playlist: any) => ({
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
public: playlist.public,
|
||||
collaborative: playlist.collaborative,
|
||||
owner: playlist.owner?.display_name || '',
|
||||
total_tracks: playlist.tracks?.total || 0,
|
||||
image_url: playlist.images?.[0]?.url || null,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
playlists,
|
||||
total: data.total || playlists.length,
|
||||
next: data.next || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
playlists: {
|
||||
type: 'array',
|
||||
description: "User's playlists",
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Spotify playlist ID' },
|
||||
name: { type: 'string', description: 'Playlist name' },
|
||||
description: { type: 'string', description: 'Playlist description' },
|
||||
public: { type: 'boolean', description: 'Whether public' },
|
||||
collaborative: { type: 'boolean', description: 'Whether collaborative' },
|
||||
owner: { type: 'string', description: 'Owner display name' },
|
||||
total_tracks: { type: 'number', description: 'Number of tracks' },
|
||||
image_url: { type: 'string', description: 'Cover image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number', description: 'Total number of playlists' },
|
||||
next: { type: 'string', description: 'URL for next page', optional: true },
|
||||
},
|
||||
}
|
||||
70
apps/sim/tools/spotify/get_user_profile.ts
Normal file
70
apps/sim/tools/spotify/get_user_profile.ts
Normal file
@@ -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<SpotifyGetUserProfileResponse> => {
|
||||
const user = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: user.id,
|
||||
display_name: user.display_name || null,
|
||||
followers: user.followers?.total || 0,
|
||||
image_url: user.images?.[0]?.url || null,
|
||||
external_url: user.external_urls?.spotify || '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'User ID' },
|
||||
display_name: { type: 'string', description: 'Display name' },
|
||||
followers: { type: 'number', description: 'Number of followers' },
|
||||
image_url: { type: 'string', description: 'Profile image URL' },
|
||||
external_url: { type: 'string', description: 'Spotify URL' },
|
||||
},
|
||||
}
|
||||
92
apps/sim/tools/spotify/index.ts
Normal file
92
apps/sim/tools/spotify/index.ts
Normal file
@@ -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'
|
||||
52
apps/sim/tools/spotify/pause.ts
Normal file
52
apps/sim/tools/spotify/pause.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyPauseParams, SpotifyPauseResponse } from './types'
|
||||
|
||||
export const spotifyPauseTool: ToolConfig<SpotifyPauseParams, SpotifyPauseResponse> = {
|
||||
id: 'spotify_pause',
|
||||
name: 'Spotify Pause',
|
||||
description: 'Pause playback on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID to pause. If not provided, pauses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player/pause'
|
||||
if (params.device_id) {
|
||||
url += `?device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyPauseResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether playback was paused' },
|
||||
},
|
||||
}
|
||||
94
apps/sim/tools/spotify/play.ts
Normal file
94
apps/sim/tools/spotify/play.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifyPlayParams, SpotifyPlayResponse } from './types'
|
||||
|
||||
export const spotifyPlayTool: ToolConfig<SpotifyPlayParams, SpotifyPlayResponse> = {
|
||||
id: 'spotify_play',
|
||||
name: 'Spotify Play',
|
||||
description:
|
||||
'Start or resume playback on Spotify. Can play specific tracks, albums, or playlists.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID to play on. If not provided, plays on active device.',
|
||||
},
|
||||
context_uri: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Spotify URI of album, artist, or playlist to play (e.g., "spotify:album:xxx")',
|
||||
},
|
||||
uris: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated track URIs to play (e.g., "spotify:track:xxx,spotify:track:yyy")',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifyPlayResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether playback started successfully' },
|
||||
},
|
||||
}
|
||||
57
apps/sim/tools/spotify/remove_saved_albums.ts
Normal file
57
apps/sim/tools/spotify/remove_saved_albums.ts
Normal file
@@ -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<SpotifyRemoveSavedAlbumsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether albums were removed' },
|
||||
},
|
||||
}
|
||||
57
apps/sim/tools/spotify/remove_saved_audiobooks.ts
Normal file
57
apps/sim/tools/spotify/remove_saved_audiobooks.ts
Normal file
@@ -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<SpotifyRemoveSavedAudiobooksResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether audiobooks were removed' },
|
||||
},
|
||||
}
|
||||
57
apps/sim/tools/spotify/remove_saved_episodes.ts
Normal file
57
apps/sim/tools/spotify/remove_saved_episodes.ts
Normal file
@@ -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<SpotifyRemoveSavedEpisodesResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether episodes were removed' },
|
||||
},
|
||||
}
|
||||
57
apps/sim/tools/spotify/remove_saved_shows.ts
Normal file
57
apps/sim/tools/spotify/remove_saved_shows.ts
Normal file
@@ -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<SpotifyRemoveSavedShowsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether shows were removed' },
|
||||
},
|
||||
}
|
||||
63
apps/sim/tools/spotify/remove_saved_tracks.ts
Normal file
63
apps/sim/tools/spotify/remove_saved_tracks.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyRemoveSavedTracksParams {
|
||||
accessToken: string
|
||||
trackIds: string
|
||||
}
|
||||
|
||||
interface SpotifyRemoveSavedTracksResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyRemoveSavedTracksTool: ToolConfig<
|
||||
SpotifyRemoveSavedTracksParams,
|
||||
SpotifyRemoveSavedTracksResponse
|
||||
> = {
|
||||
id: 'spotify_remove_saved_tracks',
|
||||
name: 'Spotify Remove Saved Tracks',
|
||||
description: "Remove tracks from the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
trackIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated track IDs to remove (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/tracks',
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
ids: params.trackIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyRemoveSavedTracksResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether tracks were removed successfully' },
|
||||
},
|
||||
}
|
||||
65
apps/sim/tools/spotify/remove_tracks_from_playlist.ts
Normal file
65
apps/sim/tools/spotify/remove_tracks_from_playlist.ts
Normal file
@@ -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<SpotifyRemoveTracksFromPlaylistResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
snapshot_id: data.snapshot_id,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID after modification' },
|
||||
},
|
||||
}
|
||||
91
apps/sim/tools/spotify/reorder_playlist_items.ts
Normal file
91
apps/sim/tools/spotify/reorder_playlist_items.ts
Normal file
@@ -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<string, any> = {
|
||||
range_start: params.range_start,
|
||||
insert_before: params.insert_before,
|
||||
range_length: params.range_length || 1,
|
||||
}
|
||||
if (params.snapshot_id) body.snapshot_id = params.snapshot_id
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<SpotifyReorderPlaylistItemsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: { snapshot_id: data.snapshot_id || '' },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID' },
|
||||
},
|
||||
}
|
||||
69
apps/sim/tools/spotify/replace_playlist_items.ts
Normal file
69
apps/sim/tools/spotify/replace_playlist_items.ts
Normal file
@@ -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<SpotifyReplacePlaylistItemsResponse> => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: { snapshot_id: data.snapshot_id || '' },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
snapshot_id: { type: 'string', description: 'New playlist snapshot ID' },
|
||||
},
|
||||
}
|
||||
55
apps/sim/tools/spotify/save_albums.ts
Normal file
55
apps/sim/tools/spotify/save_albums.ts
Normal file
@@ -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<SpotifySaveAlbumsParams, SpotifySaveAlbumsResponse> =
|
||||
{
|
||||
id: 'spotify_save_albums',
|
||||
name: 'Spotify Save Albums',
|
||||
description: "Save albums to the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
albumIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Comma-separated album IDs (max 20)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.spotify.com/v1/me/albums',
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
ids: params.albumIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 20),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveAlbumsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether albums were saved' },
|
||||
},
|
||||
}
|
||||
57
apps/sim/tools/spotify/save_audiobooks.ts
Normal file
57
apps/sim/tools/spotify/save_audiobooks.ts
Normal file
@@ -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<SpotifySaveAudiobooksResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether audiobooks were saved' },
|
||||
},
|
||||
}
|
||||
57
apps/sim/tools/spotify/save_episodes.ts
Normal file
57
apps/sim/tools/spotify/save_episodes.ts
Normal file
@@ -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<SpotifySaveEpisodesResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether episodes were saved' },
|
||||
},
|
||||
}
|
||||
54
apps/sim/tools/spotify/save_shows.ts
Normal file
54
apps/sim/tools/spotify/save_shows.ts
Normal file
@@ -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<SpotifySaveShowsParams, SpotifySaveShowsResponse> = {
|
||||
id: 'spotify_save_shows',
|
||||
name: 'Spotify Save Shows',
|
||||
description: "Save podcast shows to the user's library.",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
showIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Comma-separated show IDs (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.showIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/shows?ids=${ids}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveShowsResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether shows were saved' },
|
||||
},
|
||||
}
|
||||
48
apps/sim/tools/spotify/save_tracks.ts
Normal file
48
apps/sim/tools/spotify/save_tracks.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifySaveTracksParams, SpotifySaveTracksResponse } from './types'
|
||||
|
||||
export const spotifySaveTracksTool: ToolConfig<SpotifySaveTracksParams, SpotifySaveTracksResponse> =
|
||||
{
|
||||
id: 'spotify_save_tracks',
|
||||
name: 'Spotify Save Tracks',
|
||||
description: "Save tracks to the current user's library (like tracks).",
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-library-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
trackIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated Spotify track IDs to save (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.spotify.com/v1/me/tracks?ids=${encodeURIComponent(params.trackIds)}`,
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySaveTracksResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the tracks were saved successfully' },
|
||||
},
|
||||
}
|
||||
157
apps/sim/tools/spotify/search.ts
Normal file
157
apps/sim/tools/spotify/search.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifySearchParams, SpotifySearchResponse } from './types'
|
||||
|
||||
export const spotifySearchTool: ToolConfig<SpotifySearchParams, SpotifySearchResponse> = {
|
||||
id: 'spotify_search',
|
||||
name: 'Spotify Search',
|
||||
description:
|
||||
'Search for tracks, albums, artists, or playlists on Spotify. Returns matching results based on the query.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
},
|
||||
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query (e.g., "Bohemian Rhapsody", "artist:Queen", "genre:rock")',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 'track',
|
||||
description:
|
||||
'Type of results: track, album, artist, playlist, or comma-separated (e.g., "track,artist")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifySearchResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
const tracks = (data.tracks?.items || []).map((track: any) => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artists: track.artists?.map((a: any) => a.name) || [],
|
||||
album: track.album?.name || '',
|
||||
duration_ms: track.duration_ms,
|
||||
popularity: track.popularity,
|
||||
preview_url: track.preview_url,
|
||||
external_url: track.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
const artists = (data.artists?.items || []).map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
genres: artist.genres || [],
|
||||
popularity: artist.popularity,
|
||||
followers: artist.followers?.total || 0,
|
||||
image_url: artist.images?.[0]?.url || null,
|
||||
external_url: artist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
const albums = (data.albums?.items || []).map((album: any) => ({
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
artists: album.artists?.map((a: any) => a.name) || [],
|
||||
total_tracks: album.total_tracks,
|
||||
release_date: album.release_date,
|
||||
image_url: album.images?.[0]?.url || null,
|
||||
external_url: album.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
const playlists = (data.playlists?.items || []).map((playlist: any) => ({
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
description: playlist.description,
|
||||
owner: playlist.owner?.display_name || '',
|
||||
total_tracks: playlist.tracks?.total || 0,
|
||||
image_url: playlist.images?.[0]?.url || null,
|
||||
external_url: playlist.external_urls?.spotify || '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tracks,
|
||||
artists,
|
||||
albums,
|
||||
playlists,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tracks: {
|
||||
type: 'array',
|
||||
description: 'List of matching tracks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
67
apps/sim/tools/spotify/seek.ts
Normal file
67
apps/sim/tools/spotify/seek.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySeekParams {
|
||||
accessToken: string
|
||||
position_ms: number
|
||||
device_id?: string
|
||||
}
|
||||
|
||||
interface SpotifySeekResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifySeekTool: ToolConfig<SpotifySeekParams, SpotifySeekResponse> = {
|
||||
id: 'spotify_seek',
|
||||
name: 'Spotify Seek',
|
||||
description: 'Seek to a position in the currently playing track.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
position_ms: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position in milliseconds to seek to',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifySeekResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether seek was successful' },
|
||||
},
|
||||
}
|
||||
67
apps/sim/tools/spotify/set_repeat.ts
Normal file
67
apps/sim/tools/spotify/set_repeat.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySetRepeatParams {
|
||||
accessToken: string
|
||||
state: string
|
||||
device_id?: string
|
||||
}
|
||||
|
||||
interface SpotifySetRepeatResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifySetRepeatTool: ToolConfig<SpotifySetRepeatParams, SpotifySetRepeatResponse> = {
|
||||
id: 'spotify_set_repeat',
|
||||
name: 'Spotify Set Repeat',
|
||||
description: 'Set the repeat mode for playback.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
state: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Repeat mode: "off", "track", or "context"',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifySetRepeatResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether repeat mode was set successfully' },
|
||||
},
|
||||
}
|
||||
68
apps/sim/tools/spotify/set_shuffle.ts
Normal file
68
apps/sim/tools/spotify/set_shuffle.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifySetShuffleParams {
|
||||
accessToken: string
|
||||
state: boolean
|
||||
device_id?: string
|
||||
}
|
||||
|
||||
interface SpotifySetShuffleResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifySetShuffleTool: ToolConfig<SpotifySetShuffleParams, SpotifySetShuffleResponse> =
|
||||
{
|
||||
id: 'spotify_set_shuffle',
|
||||
name: 'Spotify Set Shuffle',
|
||||
description: 'Turn shuffle on or off.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
state: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'true for shuffle on, false for off',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-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<SpotifySetShuffleResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether shuffle was set successfully' },
|
||||
},
|
||||
}
|
||||
59
apps/sim/tools/spotify/set_volume.ts
Normal file
59
apps/sim/tools/spotify/set_volume.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifySetVolumeParams, SpotifySetVolumeResponse } from './types'
|
||||
|
||||
export const spotifySetVolumeTool: ToolConfig<SpotifySetVolumeParams, SpotifySetVolumeResponse> = {
|
||||
id: 'spotify_set_volume',
|
||||
name: 'Spotify Set Volume',
|
||||
description: 'Set the playback volume on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
volume_percent: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Volume level (0 to 100)',
|
||||
},
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID. If not provided, uses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const volume = Math.min(Math.max(params.volume_percent, 0), 100)
|
||||
let url = `https://api.spotify.com/v1/me/player/volume?volume_percent=${volume}`
|
||||
if (params.device_id) {
|
||||
url += `&device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySetVolumeResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether volume was set' },
|
||||
},
|
||||
}
|
||||
52
apps/sim/tools/spotify/skip_next.ts
Normal file
52
apps/sim/tools/spotify/skip_next.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { SpotifySkipNextParams, SpotifySkipNextResponse } from './types'
|
||||
|
||||
export const spotifySkipNextTool: ToolConfig<SpotifySkipNextParams, SpotifySkipNextResponse> = {
|
||||
id: 'spotify_skip_next',
|
||||
name: 'Spotify Skip to Next',
|
||||
description: 'Skip to the next track on Spotify.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-modify-playback-state'],
|
||||
},
|
||||
|
||||
params: {
|
||||
device_id: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Device ID. If not provided, uses active device.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
let url = 'https://api.spotify.com/v1/me/player/next'
|
||||
if (params.device_id) {
|
||||
url += `?device_id=${params.device_id}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifySkipNextResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether skip was successful' },
|
||||
},
|
||||
}
|
||||
55
apps/sim/tools/spotify/skip_previous.ts
Normal file
55
apps/sim/tools/spotify/skip_previous.ts
Normal file
@@ -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<SpotifySkipPreviousResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether skip was successful' },
|
||||
},
|
||||
}
|
||||
69
apps/sim/tools/spotify/transfer_playback.ts
Normal file
69
apps/sim/tools/spotify/transfer_playback.ts
Normal file
@@ -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<SpotifyTransferPlaybackResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether transfer was successful' },
|
||||
},
|
||||
}
|
||||
1031
apps/sim/tools/spotify/types.ts
Normal file
1031
apps/sim/tools/spotify/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
64
apps/sim/tools/spotify/unfollow_artists.ts
Normal file
64
apps/sim/tools/spotify/unfollow_artists.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface SpotifyUnfollowArtistsParams {
|
||||
accessToken: string
|
||||
artistIds: string
|
||||
}
|
||||
|
||||
interface SpotifyUnfollowArtistsResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const spotifyUnfollowArtistsTool: ToolConfig<
|
||||
SpotifyUnfollowArtistsParams,
|
||||
SpotifyUnfollowArtistsResponse
|
||||
> = {
|
||||
id: 'spotify_unfollow_artists',
|
||||
name: 'Spotify Unfollow Artists',
|
||||
description: 'Unfollow one or more artists.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'spotify',
|
||||
requiredScopes: ['user-follow-modify'],
|
||||
},
|
||||
|
||||
params: {
|
||||
artistIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated artist IDs to unfollow (max 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const ids = params.artistIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.slice(0, 50)
|
||||
.join(',')
|
||||
return `https://api.spotify.com/v1/me/following?type=artist&ids=${ids}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyUnfollowArtistsResponse> => {
|
||||
return {
|
||||
success: true,
|
||||
output: { success: true },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether artists were unfollowed successfully' },
|
||||
},
|
||||
}
|
||||
50
apps/sim/tools/spotify/unfollow_playlist.ts
Normal file
50
apps/sim/tools/spotify/unfollow_playlist.ts
Normal file
@@ -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<SpotifyUnfollowPlaylistResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether unfollow succeeded' },
|
||||
},
|
||||
}
|
||||
76
apps/sim/tools/spotify/update_playlist.ts
Normal file
76
apps/sim/tools/spotify/update_playlist.ts
Normal file
@@ -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<string, any> = {}
|
||||
if (params.name !== undefined) body.name = params.name
|
||||
if (params.description !== undefined) body.description = params.description
|
||||
if (params.public !== undefined) body.public = params.public
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (): Promise<SpotifyUpdatePlaylistResponse> => {
|
||||
return { success: true, output: { success: true } }
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether update succeeded' },
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user