CinePlex/js/ai-tools.js

560 lines
23 KiB
JavaScript
Raw Normal View History

import { state } from './state.js';
import { getFromDB, clearStore, addItemsToStore } from './db.js';
import { showNotification, _ } from './utils.js';
import { switchView, showItemDetails, addStreamToList, downloadM3U, generateStatistics, toggleFavorite, loadRecommendations, applyFilters as applyUIFilters, clearAllFavorites, clearRecommendations, loadInitialContent, loadContent, clearAllHistory } from './ui.js';
import { fetchTMDB } from './api.js';
import { updateAllTokens, addPlexToken } from './plex.js';
import { config } from './config.js';
export class AITools {
constructor(chatInstance) {
this.chat = chatInstance;
this.genreCache = { movie: null, tv: null };
}
get toolDefinitions() {
return [
{
type: 'function',
function: {
name: 'search_library',
2025-07-30 11:34:30 +02:00
description: _('aiToolSearchLibraryDesc'),
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: _('aiToolSearchLibraryQueryDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolSearchLibraryTypeDesc') }
},
required: ['query']
}
}
},
{
type: 'function',
function: {
name: 'navigate_to_page',
description: _('aiToolNavigateToPageDesc'),
parameters: {
type: 'object',
properties: {
page: {
type: 'string',
enum: ['movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator'],
description: _('aiToolNavigateToPagePageDesc')
}
},
required: ['page']
}
}
},
{
type: 'function',
function: {
name: 'get_user_stats',
description: _('aiToolGetUserStatsDesc'),
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'show_item_details',
description: _('aiToolShowItemDetailsDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolShowItemDetailsTitleDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolShowItemDetailsTypeDesc') }
},
required: ['title', 'type']
}
}
},
{
type: 'function',
function: {
name: 'add_to_playlist',
description: _('aiToolAddToPlaylistDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolAddToPlaylistTitleDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolAddToPlaylistTypeDesc') }
},
required: ['title', 'type']
}
}
},
{
type: 'function',
function: {
name: 'check_and_download_titles_list',
description: _('aiToolCheckAndDownloadDesc'),
parameters: {
type: 'object',
properties: {
titles: {
type: 'array',
items: { type: 'string' },
description: _('aiToolGenerateM3UFromTitlesTitlesDesc')
},
type: {
type: 'string',
enum: ['movie', 'series'],
description: _('aiToolGenerateM3UTypeDesc')
},
filename: { type: 'string', description: _('aiToolGenerateM3UFilenameDesc') }
},
required: ['titles', 'type']
}
}
},
{
type: 'function',
function: {
name: 'toggle_favorite',
description: _('aiToolToggleFavoriteDesc'),
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: _('aiToolToggleFavoriteTitleDesc') },
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolToggleFavoriteTypeDesc') }
},
required: ['title', 'type']
}
}
},
{
type: 'function',
function: {
name: 'get_recommendations',
description: _('aiToolGetRecommendationsDesc'),
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'apply_filters',
description: _('aiToolApplyFiltersDesc'),
parameters: {
type: 'object',
properties: {
type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolApplyFiltersTypeDesc') },
genre: { type: 'string', description: _('aiToolApplyFiltersGenreDesc') },
year: { type: 'string', description: _('aiToolApplyFiltersYearDesc') },
sort: { type: 'string', enum: ['popularity.desc', 'vote_average.desc', 'release_date.desc', 'first_air_date.desc'], description: _('aiToolApplyFiltersSortDesc') }
},
required: ['type']
}
}
},
{
type: 'function',
function: {
name: 'play_music_by_artist',
description: _('aiToolPlayMusicByArtistDesc'),
parameters: {
type: 'object',
properties: {
artist_name: { type: 'string', description: _('aiToolPlayMusicByArtistNameDesc') }
},
required: ['artist_name']
}
}
},
{
type: 'function',
function: {
name: 'clear_chat_history',
description: _('aiToolClearChatHistoryDesc'),
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'delete_database',
description: _('aiToolDeleteDatabaseDesc'),
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'update_all_tokens',
description: _('aiToolUpdateAllTokensDesc'),
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'add_plex_token',
description: _('aiToolAddPlexTokenDesc'),
parameters: {
type: 'object',
properties: {
token: { type: 'string', description: _('aiToolAddPlexTokenTokenDesc') }
},
required: ['token']
}
}
},
{
type: 'function',
function: {
name: 'change_region',
description: _('aiToolChangeRegionDesc'),
parameters: {
type: 'object',
properties: {
region: { type: 'string', description: _('aiToolChangeRegionRegionDesc') }
},
required: ['region']
}
}
},
{
type: 'function',
function: {
name: 'clear_all_favorites',
description: _('aiToolClearAllFavoritesDesc'),
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'clear_viewing_history',
description: "Clears the user's content viewing history from the history page.",
parameters: { type: 'object', properties: {} }
}
},
{
type: 'function',
function: {
name: 'clear_recommendations_view',
description: _('aiToolClearRecommendationsViewDesc'),
parameters: { type: 'object', properties: {} }
}
}
];
}
async "search_library"({ query, type }) {
const movieEntries = await getFromDB('movies');
const seriesEntries = await getFromDB('series');
const allContent = [
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
];
const searchTerm = query.toLowerCase().trim();
let results = allContent.filter(item => item.title.toLowerCase().includes(searchTerm));
if (type) {
results = results.filter(item => item.type === type);
}
if (results.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolSearchNotFound', query) });
}
const formattedResults = results.slice(0, 10).map(item => ({ title: item.title, year: item.year, type: item.type }));
return JSON.stringify({ success: true, count: results.length, results: formattedResults });
}
"navigate_to_page"({ page }) {
try {
switchView(page);
return JSON.stringify({ success: true, message: _('aiToolNavigateSuccess', page) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolNavigateError', page) });
}
}
async "get_user_stats"() {
try {
const movieItems = (await getFromDB('movies')).flatMap(s => s.titulos);
const seriesItems = (await getFromDB('series')).flatMap(s => s.titulos);
const artistItems = (await getFromDB('artists')).flatMap(s => s.titulos);
const stats = {
totalMovies: new Set(movieItems.map(item => item.title)).size,
totalSeries: new Set(seriesItems.map(item => item.title)).size,
totalArtists: new Set(artistItems.map(item => item.title)).size,
};
switchView('stats');
return JSON.stringify({ success: true, stats });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolStatsError') });
}
}
async "show_item_details"({ title, type }) {
const content = await this.findTmdbContent(title, type);
if (!content) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
showItemDetails(Number(content.id), content.type);
return JSON.stringify({ success: true, message: _('aiToolShowItemDetailsSuccess', title) });
}
async "add_to_playlist"({ title, type }) {
const content = await this.findLocalContent(title, type);
if (!content) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
addStreamToList(content.title, content.type);
return JSON.stringify({ success: true, message: _('aiToolAddToPlaylistSuccess', title) });
}
async "check_and_download_titles_list"({ titles, type, filename }) {
try {
if (!titles || titles.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolM3UNoTitlesProvided') });
}
showNotification(_('aiToolM3UCheckingTitles'), 'info');
const plexStore = type === 'movie' ? 'movies' : 'series';
const jellyfinStore = type === 'movie' ? 'jellyfin_movies' : 'jellyfin_series';
const plexContent = await getFromDB(plexStore);
const jellyfinContent = await getFromDB(jellyfinStore);
const allLocalItems = [
...plexContent.flatMap(server => server.titulos || []),
...jellyfinContent.flatMap(lib => lib.titulos || [])
];
const normalize = (str) => str ? str.toLowerCase().trim().replace(/[^a-z0-9]/g, '') : '';
const normalizedTitlesFromAI = new Set(titles.map(normalize));
const matchedItems = [];
const seenTitles = new Set();
for (const localItem of allLocalItems) {
const normalizedLocalTitle = normalize(localItem.title);
if (normalizedTitlesFromAI.has(normalizedLocalTitle) && !seenTitles.has(normalizedLocalTitle)) {
matchedItems.push({ title: localItem.title, type });
seenTitles.add(normalizedLocalTitle);
}
}
if (matchedItems.length === 0) {
return JSON.stringify({ success: true, message: _('aiToolM3UNoLocalMatchesForDownload') });
}
downloadM3U(matchedItems, null, filename);
return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStarted', [String(matchedItems.length), String(titles.length)]) });
} catch (error) {
return JSON.stringify({ success: false, message: `Error creating playlist: ${error.message}` });
}
}
async "toggle_favorite"({ title, type }) {
const item = await this.findTmdbContent(title, type);
if (!item) {
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
}
toggleFavorite(item.id, type);
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === type);
const message = isFavorite ? _('aiToolFavoriteAdded', title) : _('aiToolFavoriteRemoved', title);
return JSON.stringify({ success: true, message });
}
async "get_recommendations"() {
switchView('recommendations');
return JSON.stringify({ success: true, message: _('aiToolRecommendationsSuccess') });
}
async "apply_filters"({ type, genre, year, sort }) {
switchView(type === 'movie' ? 'movies' : 'series');
let genreId = '';
if (genre) {
genreId = await this.getGenreId(genre, type);
if (!genreId) {
return JSON.stringify({ success: false, message: _('aiToolApplyFiltersGenreNotFound', genre) });
}
}
state.currentParams.genre = genreId;
state.currentParams.year = year || '';
state.currentParams.sort = sort || 'popularity.desc';
applyUIFilters();
return JSON.stringify({ success: true, message: _('aiToolApplyFiltersSuccess') });
}
async "play_music_by_artist"({ artist_name }) {
if (!state.musicPlayer || !state.musicPlayer.isReady) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
}
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
const searchTerm = artist_name.toLowerCase().trim();
const artist = allArtists.find(a => a.title.toLowerCase() === searchTerm);
if (!artist) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicArtistNotFound', artist_name) });
}
state.musicPlayer.showPlayer();
const songs = await state.musicPlayer.getArtistSongs(artist);
if (!songs || songs.length === 0) {
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNoSongs', artist_name) });
}
state.musicPlayer.cancionesActuales = songs;
state.musicPlayer.playSong(0);
return JSON.stringify({ success: true, message: _('aiToolPlayMusicSuccess', artist_name) });
}
async "clear_chat_history"() {
try {
this.chat.clearHistory();
return JSON.stringify({ success: true, message: _('aiToolChatHistoryCleared') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolChatHistoryClearError', error.message) });
}
}
async "delete_database"() {
try {
if (confirm(_('aiToolConfirmDeleteDatabase'))) {
if (state.db) {
state.db.close();
}
const deleteRequest = indexedDB.deleteDatabase(config.dbName);
return new Promise((resolve) => {
deleteRequest.onsuccess = () => {
setTimeout(() => window.location.reload(), 1500);
resolve(JSON.stringify({ success: true, message: _('aiToolDatabaseDeleted') }));
};
deleteRequest.onerror = (event) => {
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', event.target.error) }));
};
deleteRequest.onblocked = () => {
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteBlocked') }));
};
});
} else {
return JSON.stringify({ success: false, message: _('aiToolDeleteDatabaseCancelled') });
}
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', error.message) });
}
}
async "update_all_tokens"() {
try {
await updateAllTokens();
return JSON.stringify({ success: true, message: _('aiToolUpdateAllTokensSuccess') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolUpdateAllTokensError', error.message) });
}
}
async "add_plex_token"({ token }) {
try {
await addPlexToken(token);
return JSON.stringify({ success: true, message: _('aiToolAddPlexTokenSuccess') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolAddPlexTokenError', error.message) });
}
}
async "change_region"({ region }) {
try {
state.settings.watchRegion = region;
await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]);
await loadInitialContent();
if (['movies', 'series', 'search'].includes(state.currentView)) {
await loadContent();
}
return JSON.stringify({ success: true, message: _('aiToolChangeRegionSuccess', region) });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolChangeRegionError', error.message) });
}
}
async "clear_all_favorites"() {
try {
clearAllFavorites();
return JSON.stringify({ success: true, message: _('aiToolFavoritesCleared') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolFavoritesClearError', error.message) });
}
}
async "clear_viewing_history"() {
try {
clearAllHistory();
return JSON.stringify({ success: true, message: "Viewing history has been cleared." });
} catch (error) {
return JSON.stringify({ success: false, message: `Failed to clear viewing history: ${error.message}` });
}
}
async "clear_recommendations_view"() {
try {
clearRecommendations();
return JSON.stringify({ success: true, message: _('aiToolRecommendationsCleared') });
} catch (error) {
return JSON.stringify({ success: false, message: _('aiToolRecommendationsClearError', error.message) });
}
}
async findLocalContent(title, type) {
const movieEntries = await getFromDB('movies');
const seriesEntries = await getFromDB('series');
const allContent = [
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
];
const searchTerm = title.toLowerCase().trim();
return allContent.find(item => item.title.toLowerCase() === searchTerm && (!type || item.type === type));
}
async findTmdbContent(title, type) {
try {
const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}`);
if (searchResults && searchResults.results.length > 0) {
const item = searchResults.results[0];
return { id: item.id, type };
}
return null;
} catch (error) {
return null;
}
}
async getGenreId(genreName, type) {
if (!this.genreCache[type]) {
try {
const data = await fetchTMDB(`genre/${type}/list`);
this.genreCache[type] = data.genres;
} catch (error) {
return null;
}
}
const genre = this.genreCache[type].find(g => g.name.toLowerCase() === genreName.toLowerCase());
return genre ? genre.id : null;
}
async executeTool(toolCall) {
const functionName = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);
if (typeof this[functionName] === 'function') {
this.chat.addTypingIndicator();
try {
const result = await this[functionName](args);
return result;
} catch (error) {
const errorMessage = _('aiToolExecutionError', functionName, error.message);
this.chat.addMessage(errorMessage, 'tool-result', true, functionName);
return JSON.stringify({ success: false, error: errorMessage });
} finally {
this.chat.removeTypingIndicator();
}
} else {
const errorMessage = _('aiToolUnknown', functionName);
this.chat.addMessage(errorMessage, 'tool-result', true, functionName);
return JSON.stringify({ success: false, error: errorMessage });
}
}
}