diff --git a/ts/packages/agents/player/src/agent/playerHandlers.ts b/ts/packages/agents/player/src/agent/playerHandlers.ts index 91aa3ae00..700fbb231 100644 --- a/ts/packages/agents/player/src/agent/playerHandlers.ts +++ b/ts/packages/agents/player/src/agent/playerHandlers.ts @@ -378,6 +378,7 @@ async function getPlayerActionCompletion( case "deletePlaylist": case "addCurrentTrackToPlaylist": case "addToPlaylistFromCurrentTrackList": + case "addSongsToPlaylist": if (propertyName === "parameters.name") { if (userData.data.playlists === undefined) { await getPlaylistsFromUserData( diff --git a/ts/packages/agents/player/src/agent/playerSchema.ts b/ts/packages/agents/player/src/agent/playerSchema.ts index 7f8338432..ed1edaced 100644 --- a/ts/packages/agents/player/src/agent/playerSchema.ts +++ b/ts/packages/agents/player/src/agent/playerSchema.ts @@ -32,11 +32,19 @@ export type PlayerActions = | DeletePlaylistAction | AddCurrentTrackToPlaylistAction | AddToPlaylistFromCurrentTrackListAction + | AddSongsToPlaylistAction | GetQueueAction; export type PlayerEntities = MusicDevice; export type MusicDevice = string; +// Specification for a song by title and optional artist/album +export interface SongSpecification { + trackName: string; + artist?: string; + albumName?: string; +} + // Use playRandom when the user asks for some music to play export interface PlayRandomAction { actionName: "playRandom"; @@ -235,12 +243,14 @@ export interface GetFavoritesAction { }; } -// create a new empty playlist +// create a new playlist, optionally with a list of songs specified by title and artist export interface CreatePlaylistAction { actionName: "createPlaylist"; parameters: { // name of playlist to create name: string; + // optional list of songs to add to the playlist when creating it + songs?: SongSpecification[]; }; } @@ -276,6 +286,18 @@ export interface AddToPlaylistFromCurrentTrackListAction { }; } +// add songs to a playlist by specifying track names and optional artists +// this action searches for each song and adds it to the playlist +export interface AddSongsToPlaylistAction { + actionName: "addSongsToPlaylist"; + parameters: { + // name of playlist to add songs to + name: string; + // list of songs to add, each specified by track name and optional artist/album + songs: SongSpecification[]; + }; +} + // set the current track list to the queue of upcoming tracks export interface GetQueueAction { actionName: "getQueue"; diff --git a/ts/packages/agents/player/src/client.ts b/ts/packages/agents/player/src/client.ts index 49f0949fd..b836761c6 100644 --- a/ts/packages/agents/player/src/client.ts +++ b/ts/packages/agents/player/src/client.ts @@ -18,6 +18,8 @@ import { GetFromCurrentPlaylistListAction, AddCurrentTrackToPlaylistAction, AddToPlaylistFromCurrentTrackListAction, + AddSongsToPlaylistAction, + SongSpecification, } from "./agent/playerSchema.js"; import { createTokenProvider } from "./defaultTokenProvider.js"; import chalk from "chalk"; @@ -536,6 +538,49 @@ export async function searchForPlaylists( } } +// Search for tracks from a list of song specifications and return their URIs +async function searchSongsAndGetUris( + songs: SongSpecification[], + context: IClientContext, +): Promise<{ uris: string[]; notFound: string[] }> { + const uris: string[] = []; + const notFound: string[] = []; + + for (const song of songs) { + // Build search query from track name and optional artist/album + let queryString = song.trackName; + if (song.artist) { + queryString += ` artist:${song.artist}`; + } + if (song.albumName) { + queryString += ` album:${song.albumName}`; + } + + const trackCollection = await searchTracks(queryString, context); + if (trackCollection) { + const tracks = trackCollection.getTracks(); + if (tracks.length > 0) { + // Take the first (best) match + uris.push(tracks[0].uri); + } else { + notFound.push( + song.artist + ? `${song.trackName} by ${song.artist}` + : song.trackName, + ); + } + } else { + notFound.push( + song.artist + ? `${song.trackName} by ${song.artist}` + : song.trackName, + ); + } + } + + return { uris, notFound }; +} + async function playTrackCollection( trackCollection: ITrackCollection, clientContext: IClientContext, @@ -1300,17 +1345,40 @@ export async function handleCall( */ case "createPlaylist": { const name = action.parameters.name; - // create empty playlist + const songs = action.parameters.songs; + + let resultMessage = `playlist ${name} created`; + let uris: string[] = []; + + // If songs are specified, search for them first + if (songs && songs.length > 0) { + const searchResult = await searchSongsAndGetUris( + songs, + clientContext, + ); + uris = searchResult.uris; + + if (uris.length > 0) { + resultMessage += ` with ${uris.length} song${uris.length > 1 ? "s" : ""}`; + } + + if (searchResult.notFound.length > 0) { + resultMessage += `\nCouldn't find: ${searchResult.notFound.join(", ")}`; + } + } + + // Create the playlist with the URIs (or empty if no songs) await createPlaylist( clientContext.service, name, clientContext.service.retrieveUser().id!, - [], + uris, name, ); - console.log(`playlist ${name} created`); + + console.log(resultMessage); return createActionResultFromTextDisplay( - chalk.magentaBright(`playlist ${name} created`), + chalk.magentaBright(resultMessage), ); } case "deletePlaylist": { @@ -1421,6 +1489,55 @@ export async function handleCall( chalk.magentaBright(resultString), ); } + case "addSongsToPlaylist": { + const addAction = action as AddSongsToPlaylistAction; + const playlistName = addAction.parameters.name; + const songs = addAction.parameters.songs; + + if (clientContext.userData === undefined) { + return createErrorActionResult("No user data found"); + } + + // Find the playlist + const playlists = await getPlaylistsFromUserData( + clientContext.service, + clientContext.userData!.data, + ); + const playlist = playlists?.find((pl) => { + return pl.name + .toLowerCase() + .includes(playlistName.toLowerCase()); + }); + if (!playlist) { + return createErrorActionResult( + `playlist ${playlistName} not found`, + ); + } + + // Search for the songs and get their URIs + const { uris, notFound } = await searchSongsAndGetUris( + songs, + clientContext, + ); + + if (uris.length === 0) { + return createErrorActionResult( + `Could not find any of the specified songs`, + ); + } + + // Add the tracks to the playlist + await addTracksToPlaylist(clientContext.service, playlist.id, uris); + + let resultMessage = `Added ${uris.length} song${uris.length > 1 ? "s" : ""} to playlist ${playlist.name}`; + if (notFound.length > 0) { + resultMessage += `\nCouldn't find: ${notFound.join(", ")}`; + } + + return createActionResultFromTextDisplay( + chalk.magentaBright(resultMessage), + ); + } default: return createErrorActionResult( `Action not supported: ${(action as any).actionName}`,