Skip to content
1 change: 1 addition & 0 deletions ts/packages/agents/player/src/agent/playerHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
24 changes: 23 additions & 1 deletion ts/packages/agents/player/src/agent/playerSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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[];
};
}

Expand Down Expand Up @@ -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";
Expand Down
125 changes: 121 additions & 4 deletions ts/packages/agents/player/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
GetFromCurrentPlaylistListAction,
AddCurrentTrackToPlaylistAction,
AddToPlaylistFromCurrentTrackListAction,
AddSongsToPlaylistAction,
SongSpecification,
} from "./agent/playerSchema.js";
import { createTokenProvider } from "./defaultTokenProvider.js";
import chalk from "chalk";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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}`,
Expand Down
Loading