import { exec } from 'child_process';
import { promisify } from 'util';
import { z } from 'zod';
import { ejecutarPeticionPlayer } from '../core/spotify.js';
const execAsync = promisify(exec);
const spotifyPlayer = {
nombre: 'spotifyPlayer',
descripcion: `Controla reproduccion de Spotify. MANEJO DE ERRORES:
- Si error "No hay dispositivo activo": 1) Usa spotifyInfo(accion="devices") para obtener lista. 2) Si hay dispositivos, usa spotifyPlayer(accion="transfer", dispositivo="ID") para activarlo y luego reintenta play. 3) Si no hay dispositivos, usa spotifyPlayer(accion="openApp"), espera que cargue, repite desde paso 1.
- Si transfer falla o no hay dispositivos despues de abrir: Pide al usuario que reproduzca algo manualmente en Spotify para activar la sesion.
- Si error "Premium requerido": Informa que necesita Spotify Premium.
- Para reproducir: Primero busca con spotifyInfo(accion="search"), luego usa el ID con play.
FLUJO RECOMENDADO cuando no reproduce: devices -> transfer(dispositivo=ID) -> play. Si falla, openApp -> esperar -> devices -> transfer -> play.`,
esquema: {
accion: z.enum(['play', 'pause', 'resume', 'next', 'prev', 'volume', 'shuffle', 'repeat', 'seek', 'queue', 'transfer', 'playLiked', 'openApp'])
.describe('play=reproducir(uri o tipo+id), pause/resume, next/prev, volume(valor:0-100), shuffle(valor:bool), repeat(valor:track/context/off), seek(valor:ms), queue=agregar a cola, transfer=cambiar dispositivo, playLiked=reproducir Me gusta, openApp=abrir Spotify(valor:true=web)'),
uri: z.string().optional().describe('URI completo de Spotify ej: spotify:track:ID (alternativa a tipo+id)'),
tipo: z.enum(['track', 'album', 'artist', 'playlist']).optional().describe('Tipo de contenido para play/queue'),
id: z.string().optional().describe('ID del contenido (obtener de spotifyInfo search)'),
valor: z.union([z.number(), z.boolean(), z.string()]).optional().describe('Valor según acción: volume(0-100), shuffle(bool), repeat(track/context/off), seek(ms), openApp(true=forzar web)'),
dispositivo: z.string().optional().describe('ID del dispositivo (obtener de spotifyInfo devices)'),
enContexto: z.boolean().optional().describe('Para tracks: reproducir en contexto del álbum (default: true, false=solo la canción)'),
},
ejecutar: async (args, _extra) => {
const { accion, uri, tipo, id, valor, dispositivo, enContexto = true } = args;
const device = dispositivo || '';
switch (accion) {
case 'play': {
if (!(uri || (tipo && id))) {
return { content: [{ type: 'text', text: 'Error: Proporciona uri o tipo+id' }] };
}
const spotifyUri = uri || `spotify:${tipo}:${id}`;
let mensaje = `▶️ Reproduciendo ${tipo || 'música'}`;
const res = await ejecutarPeticionPlayer(async (api) => {
if (tipo === 'track' && id && enContexto) {
const track = await api.tracks.get(id);
if (track.album?.uri) {
await api.player.startResumePlayback(device, track.album.uri, undefined, { uri: spotifyUri });
mensaje = `▶️ "${track.name}" en álbum "${track.album.name}"`;
return;
}
}
if (tipo === 'track') {
await api.player.startResumePlayback(device, undefined, [spotifyUri]);
}
else {
await api.player.startResumePlayback(device, spotifyUri);
}
}, 'play');
return { content: [{ type: 'text', text: res.ok ? mensaje : res.error }] };
}
case 'pause': {
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.pausePlayback(device); }, 'pause');
return { content: [{ type: 'text', text: res.ok ? '⏸️ Pausado' : res.error }] };
}
case 'resume': {
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.startResumePlayback(device); }, 'resume');
return { content: [{ type: 'text', text: res.ok ? '▶️ Reanudado' : res.error }] };
}
case 'next': {
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.skipToNext(device); }, 'next');
return { content: [{ type: 'text', text: res.ok ? '⏭️ Siguiente' : res.error }] };
}
case 'prev': {
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.skipToPrevious(device); }, 'prev');
return { content: [{ type: 'text', text: res.ok ? '⏮️ Anterior' : res.error }] };
}
case 'volume': {
const vol = typeof valor === 'number' ? valor : 50;
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.setPlaybackVolume(Math.min(100, Math.max(0, vol)), device); }, 'volume');
return { content: [{ type: 'text', text: res.ok ? `🔊 Volumen: ${vol}%` : res.error }] };
}
case 'shuffle': {
const activar = typeof valor === 'boolean' ? valor : true;
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.togglePlaybackShuffle(activar, device); }, 'shuffle');
return { content: [{ type: 'text', text: res.ok ? `🔀 Aleatorio ${activar ? 'activado' : 'desactivado'}` : res.error }] };
}
case 'repeat': {
const modo = typeof valor === 'string' && ['track', 'context', 'off'].includes(valor) ? valor : 'off';
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.setRepeatMode(modo, device); }, 'repeat');
const modos = { track: 'canción', context: 'álbum/playlist', off: 'desactivado' };
return { content: [{ type: 'text', text: res.ok ? `🔁 Repetición: ${modos[modo]}` : res.error }] };
}
case 'seek': {
const posMs = typeof valor === 'number' ? valor : 0;
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.seekToPosition(posMs, device); }, 'seek');
const min = Math.floor(posMs / 60000);
const seg = Math.floor((posMs % 60000) / 1000);
return { content: [{ type: 'text', text: res.ok ? `⏩ Posición: ${min}:${seg.toString().padStart(2, '0')}` : res.error }] };
}
case 'queue': {
const queueUri = uri || (tipo && id ? `spotify:${tipo}:${id}` : undefined);
if (!queueUri)
return { content: [{ type: 'text', text: 'Error: Proporciona uri o tipo+id' }] };
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.addItemToPlaybackQueue(queueUri, device); }, 'queue');
return { content: [{ type: 'text', text: res.ok ? '➕ Agregado a la cola' : res.error }] };
}
case 'transfer': {
if (!dispositivo)
return { content: [{ type: 'text', text: 'Error: Proporciona dispositivo' }] };
const res = await ejecutarPeticionPlayer(async (api) => { await api.player.transferPlayback([dispositivo], true); }, 'transfer');
return { content: [{ type: 'text', text: res.ok ? '📱 Reproducción transferida' : res.error }] };
}
case 'playLiked': {
const shuffle = typeof valor === 'boolean' ? valor : false;
const res = await ejecutarPeticionPlayer(async (api) => {
const perfil = await api.currentUser.profile();
if (shuffle)
await api.player.togglePlaybackShuffle(true, device);
await api.player.startResumePlayback(device, `spotify:user:${perfil.id}:collection`);
}, 'playLiked');
return { content: [{ type: 'text', text: res.ok ? `💚 Reproduciendo Me gusta${shuffle ? ' (aleatorio)' : ''}` : res.error }] };
}
case 'openApp': {
const plataforma = process.platform;
const forzarWeb = typeof valor === 'boolean' ? valor : false;
const urlWeb = 'https://open.spotify.com';
if (forzarWeb) {
await abrirEnNavegador(urlWeb, plataforma);
return { content: [{ type: 'text', text: '🌐 Abriendo Spotify Web... IMPORTANTE: Pide al usuario que espere a que cargue completamente y te avise. Luego usa spotifyInfo(accion="devices") para verificar que el dispositivo esté disponible antes de reproducir.' }] };
}
try {
if (plataforma === 'win32')
await execAsync('start spotify:');
else if (plataforma === 'darwin')
await execAsync('open -a Spotify');
else
await execAsync('spotify &');
return { content: [{ type: 'text', text: '🎵 Abriendo Spotify Desktop... IMPORTANTE: Pide al usuario que espere a que cargue completamente y te avise. Luego usa spotifyInfo(accion="devices") para verificar que el dispositivo esté disponible antes de reproducir.' }] };
}
catch {
await abrirEnNavegador(urlWeb, plataforma);
return { content: [{ type: 'text', text: '🌐 No se encontró Spotify Desktop, abriendo Spotify Web... IMPORTANTE: Pide al usuario que espere a que cargue completamente y te avise. Luego usa spotifyInfo(accion="devices") para verificar.' }] };
}
}
default:
return { content: [{ type: 'text', text: '❌ Acción no válida' }] };
}
},
};
async function abrirEnNavegador(url, plataforma) {
if (plataforma === 'win32')
await execAsync(`start "" "${url}"`);
else if (plataforma === 'darwin')
await execAsync(`open "${url}"`);
else
await execAsync(`xdg-open "${url}"`);
}
export const herramientasPlayer = [spotifyPlayer];