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', description: _('aiToolSearchLibraryDesc_v2'), 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 }); } } }