Files
alice-backend/server/device.mjs
T

61 lines
2.3 KiB
JavaScript

import { randomUUID } from 'node:crypto';
import WebSocket from 'ws';
import { searchSpotify, playlistTool } from './spotify.mjs';
const spotifyUri = /^spotify:(track|artist|album|playlist|episode):[A-Za-z0-9]{22}$/;
export function deviceTools(client, signal) {
const pending = new Map();
signal.addEventListener('abort', () => {
for (const { reject, timeout } of pending.values()) {
clearTimeout(timeout);
reject(new Error('device_disconnected'));
}
pending.clear();
}, { once: true });
function request(name, args) {
if (signal.aborted || client.readyState !== WebSocket.OPEN) throw new Error('device_disconnected');
const id = randomUUID();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
pending.delete(id);
reject(new Error('device_tool_timeout'));
}, 17000);
pending.set(id, { resolve, reject, timeout });
client.send(JSON.stringify({ type: 'device_tool', id, name, arguments: args }));
});
}
return {
accept(message) {
if (message?.type !== 'device_tool_result') return false;
const call = pending.get(message.id);
if (!call) return true;
clearTimeout(call.timeout);
pending.delete(message.id);
call.resolve(message.result ?? { error: 'empty_device_result' });
return true;
},
async execute(name, args) {
if (name === 'spotify_playlist') return playlistTool(args, signal);
const enqueue = name === 'spotify_control' && args.action === 'enqueue';
if (name !== 'spotify_play' && !enqueue) return request(name, args);
let selected;
if (args.uri) {
if (!spotifyUri.test(args.uri)) return { error: 'invalid_spotify_uri' };
selected = { uri: args.uri };
} else {
try {
selected = (await searchSpotify(args.query, signal, enqueue ? 'track' : args.type))[0];
} catch {
return { error: 'spotify_search_unavailable', hint: 'Подключи Spotify в пульте Telegram и повтори запрос.' };
}
}
if (!selected) return { error: 'spotify_not_found' };
const result = await request(name, enqueue ? { action: 'enqueue', uri: selected.uri } : { uri: selected.uri });
return { ...result, selection: selected };
},
};
}