import { state } from './state.js'; import { getFromDB, clearStore, addItemsToStore, exportDatabase, importDatabase } from './db.js'; import { showNotification, _ } from './utils.js'; import { switchView, showItemDetails, addStreamToList, downloadM3U, generateStatistics, toggleFavorite, loadRecommendations, applyFilters as applyUIFilters, clearAllFavorites, clearRecommendations, loadInitialContent, loadContent, clearAllHistory, applyTheme, applyHeroVisibility, getTrailerKey, showTrailer, renderGrid, showActorDetails, isContentAvailableLocally } 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 [ { name: 'search_library', description: _('aiToolSearchLibraryDesc'), parameters: { type: 'object', properties: { query: { type: 'string', description: _('aiToolSearchLibraryQueryParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolSearchLibraryTypeParamDesc') }, resolution: { type: 'string', description: _('aiToolSearchLibraryResolutionParamDesc') }, container: { type: 'string', description: _('aiToolSearchLibraryContainerParamDesc') } }, required: [] } }, { name: 'navigate_to_page', description: _('aiToolNavigateToPageDesc'), parameters: { type: 'object', properties: { page: { type: 'string', enum: ['movies', 'series', 'stats', 'favorites', 'history', 'recommendations', 'photos', 'providers', 'm3u-generator', 'music'], description: _('aiToolNavigateToPagePageParamDesc') } }, required: ['page'] } }, { name: 'get_user_stats', description: _('aiToolGetUserStatsDesc'), parameters: { type: 'object', properties: {} } }, { name: 'show_item_details', description: _('aiToolShowItemDetailsDesc'), parameters: { type: 'object', properties: { title: { type: 'string', description: _('aiToolShowItemDetailsTitleParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolShowItemDetailsTypeParamDesc') } }, required: ['title', 'type'] } }, { name: 'add_to_playlist', description: _('aiToolAddToPlaylistDesc'), parameters: { type: 'object', properties: { title: { type: 'string', description: _('aiToolAddToPlaylistTitleParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolAddToPlaylistTypeParamDesc') } }, required: ['title', 'type'] } }, { name: 'download_single_movie_m3u', description: _('aiToolDownloadSingleMovieM3UDesc'), parameters: { type: 'object', properties: { movie_title: { type: 'string', description: _('aiToolDownloadSingleMovieM3UTitleParamDesc') }, year: { type: 'string', description: _('aiToolDownloadSingleMovieM3UYearParamDesc') } }, required: ['movie_title'] } }, { name: 'download_series_season_m3u', description: _('aiToolDownloadSeriesSeasonM3UDesc'), parameters: { type: 'object', properties: { series_title: { type: 'string', description: _('aiToolDownloadSeriesSeasonM3UTitleParamDesc') }, season_number: { type: 'number', description: _('aiToolDownloadSeriesSeasonM3USeasonParamDesc') }, year: { type: 'string', description: _('aiToolDownloadSeriesSeasonM3UYearParamDesc') } }, required: ['series_title', 'season_number'] } }, { name: 'check_and_download_titles_list', description: _('aiToolCheckAndDownloadDesc'), parameters: { type: 'object', properties: { titles: { type: 'array', items: { type: 'string' }, description: _('aiToolCheckAndDownloadTitlesParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolCheckAndDownloadTypeParamDesc') }, filename: { type: 'string', description: _('aiToolCheckAndDownloadFilenameParamDesc') } }, required: ['titles', 'type'] } }, { name: 'toggle_favorite', description: _('aiToolToggleFavoriteDesc'), parameters: { type: 'object', properties: { title: { type: 'string', description: _('aiToolToggleFavoriteTitleParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolToggleFavoriteTypeParamDesc') } }, required: ['title', 'type'] } }, { name: 'get_recommendations', description: _('aiToolGetRecommendationsDesc'), parameters: { type: 'object', properties: {} } }, { name: 'apply_filters', description: _('aiToolApplyFiltersDesc'), parameters: { type: 'object', properties: { type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolApplyFiltersTypeParamDesc') }, genre: { type: 'string', description: _('aiToolApplyFiltersGenreParamDesc') }, year: { type: 'string', description: _('aiToolApplyFiltersYearParamDesc') }, sort: { type: 'string', enum: ['popularity.desc', 'vote_average.desc', 'release_date.desc', 'first_air_date.desc'], description: _('aiToolApplyFiltersSortParamDesc') } }, required: ['type'] } }, { name: 'list_available_music_genres', description: _('aiToolListAvailableMusicGenresDesc'), parameters: { type: 'object', properties: {} } }, { name: 'search_music_by_genre', description: _('aiToolSearchMusicByGenreDesc'), parameters: { type: 'object', properties: { genre_name: { type: 'string', description: _('aiToolSearchMusicByGenreNameParamDesc') } }, required: ['genre_name'] } }, { name: 'play_music_by_artist', description: _('aiToolPlayMusicByArtistDesc'), parameters: { type: 'object', properties: { artist_name: { type: 'string', description: _('aiToolPlayMusicByArtistNameParamDesc') } }, required: ['artist_name'] } }, { name: 'clear_chat_history', description: _('aiToolClearChatHistoryDesc'), parameters: { type: 'object', properties: {} } }, { name: 'delete_database', description: _('aiToolDeleteDatabaseDesc'), parameters: { type: 'object', properties: {} } }, { name: 'update_all_tokens', description: _('aiToolUpdateAllTokensDesc'), parameters: { type: 'object', properties: {} } }, { name: 'add_plex_token', description: _('aiToolAddPlexTokenDesc'), parameters: { type: 'object', properties: { token: { type: 'string', description: _('aiToolAddPlexTokenTokenParamDesc') } }, required: ['token'] } }, { name: 'change_region', description: _('aiToolChangeRegionDesc'), parameters: { type: 'object', properties: { region: { type: 'string', description: _('aiToolChangeRegionRegionParamDesc') } }, required: ['region'] } }, { name: 'clear_all_favorites', description: _('aiToolClearAllFavoritesDesc'), parameters: { type: 'object', properties: {} } }, { name: 'clear_viewing_history', description: _('aiToolClearViewingHistoryDesc'), parameters: { type: 'object', properties: {} } }, { name: 'clear_recommendations_view', description: _('aiToolClearRecommendationsViewDesc'), parameters: { type: 'object', properties: {} } }, { name: 'view_history', description: _('aiToolViewHistoryDesc'), parameters: { type: 'object', properties: {} } }, { name: 'view_favorites', description: _('aiToolViewFavoritesDesc'), parameters: { type: 'object', properties: {} } }, { name: 'play_song', description: _('aiToolPlaySongDesc'), parameters: { type: 'object', properties: { title: { type: 'string', description: _('aiToolPlaySongTitleParamDesc') }, artist: { type: 'string', description: _('aiToolPlaySongArtistParamDesc') } }, required: ['title'] } }, { name: 'toggle_light_mode', description: _('aiToolToggleLightModeDesc'), parameters: { type: 'object', properties: {} } }, { name: 'toggle_hero_section', description: _('aiToolToggleHeroSectionDesc'), parameters: { type: 'object', properties: {} } }, { name: 'export_local_database', description: _('aiToolExportLocalDatabaseDesc'), parameters: { type: 'object', properties: {} } }, { name: 'import_local_database', description: _('aiToolImportLocalDatabaseDesc'), parameters: { type: 'object', properties: {} } }, { name: 'search_tmdb_content', description: _('aiToolSearchTmdbContentDesc'), parameters: { type: 'object', properties: { query: { type: 'string', description: _('aiToolSearchTmdbContentQueryParamDesc') }, type: { type: 'string', enum: ['movie', 'series', 'person', 'all'], description: _('aiToolSearchTmdbContentTypeParamDesc') } }, required: ['query'] } }, { name: 'get_trending_content', description: _('aiToolGetTrendingContentDesc'), parameters: { type: 'object', properties: { type: { type: 'string', enum: ['movie', 'series', 'all'], description: _('aiToolGetTrendingContentTypeParamDesc') } }, required: ['type'] } }, { name: 'show_actor_details', description: _('aiToolShowActorDetailsDesc'), parameters: { type: 'object', properties: { actor_name: { type: 'string', description: _('aiToolShowActorDetailsNameParamDesc') } }, required: ['actor_name'] } }, { name: 'play_trailer', description: _('aiToolPlayTrailerDesc'), parameters: { type: 'object', properties: { title: { type: 'string', description: _('aiToolPlayTrailerTitleParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolPlayTrailerTypeParamDesc') } }, required: ['title', 'type'] } }, { name: 'check_local_availability', description: _('aiToolCheckLocalAvailabilityDesc'), parameters: { type: 'object', properties: { title: { type: 'string', description: _('aiToolCheckLocalAvailabilityTitleParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolCheckLocalAvailabilityTypeParamDesc') }, year: { type: 'string', description: _('aiToolCheckLocalAvailabilityYearParamDesc') } }, required: ['title', 'type'] } }, { name: 'get_local_series_seasons', description: _('aiToolGetLocalSeriesSeasonsDesc'), parameters: { type: 'object', properties: { series_title: { type: 'string', description: _('aiToolGetLocalSeriesSeasonsTitleParamDesc') }, year: { type: 'string', description: _('aiToolGetLocalSeriesSeasonsYearParamDesc') } }, required: ['series_title'] } }, { name: 'find_streaming_providers', description: _('aiToolFindStreamingProvidersDesc'), parameters: { type: 'object', properties: { title: { type: 'string', description: _('aiToolFindStreamingProvidersTitleParamDesc') }, type: { type: 'string', enum: ['movie', 'series'], description: _('aiToolFindStreamingProvidersTypeParamDesc') }, year: { type: 'string', description: _('aiToolFindStreamingProvidersYearParamDesc') } }, required: ['title', 'type'] } } ]; } async "search_library"({ query, type, resolution, container }) { const movieEntries = await getFromDB('movies'); const seriesEntries = await getFromDB('series'); const jellyfinMovieEntries = await getFromDB('jellyfin_movies'); const jellyfinSeriesEntries = await getFromDB('jellyfin_series'); const allContent = [ ...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))), ...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' }))), ...jellyfinMovieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))), ...jellyfinSeriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' }))) ]; let results = allContent; if (query) { const searchTerm = query.toLowerCase().trim(); results = allContent.filter(item => item.title.toLowerCase().includes(searchTerm)); } if (type) { results = results.filter(item => item.type === type); } if (resolution) { results = results.filter(item => item.resolution && item.resolution.toLowerCase() === resolution.toLowerCase()); } if (container) { results = results.filter(item => item.container && item.container.toLowerCase() === container.toLowerCase()); } 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, resolution: item.resolution, container: item.container })); 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 movieItemsPlex = (await getFromDB('movies')).flatMap(s => s.titulos); const seriesItemsPlex = (await getFromDB('series')).flatMap(s => s.titulos); const artistItemsPlex = (await getFromDB('artists')).flatMap(s => s.titulos); const movieItemsJellyfin = (await getFromDB('jellyfin_movies')).flatMap(s => s.titulos); const seriesItemsJellyfin = (await getFromDB('jellyfin_series')).flatMap(s => s.titulos); const allMovieTitles = new Set([...movieItemsPlex.map(item => item.title), ...movieItemsJellyfin.map(item => item.title)]); const allSeriesTitles = new Set([...seriesItemsPlex.map(item => item.title), ...seriesItemsJellyfin.map(item => item.title)]); const allArtistTitles = new Set(artistItemsPlex.map(item => item.title)); const plexConnections = await getFromDB('conexiones_locales'); const jellyfinConnections = await getFromDB('jellyfin_settings'); const stats = { totalMovies: allMovieTitles.size, totalSeries: allSeriesTitles.size, totalArtists: allArtistTitles.size, plexServers: plexConnections.length, jellyfinServers: jellyfinConnections.length }; 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) }); } state.aiTriggeredDetails = true; 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 "download_single_movie_m3u"({ movie_title, year }) { try { downloadM3U([{ title: movie_title, type: 'movie', year: year }], null, movie_title); return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStartedSingle', movie_title) }); } catch (error) { return JSON.stringify({ success: false, message: `Error generating M3U for ${movie_title}: ${error.message}` }); } } async "download_series_season_m3u"({ series_title, season_number, year }) { try { const filename = `${series_title.replace(/[^a-z0-9]/gi, '_')}_S${season_number}`; downloadM3U([{ title: series_title, type: 'tv', year: year, seasonNumber: season_number }], null, filename); return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStartedSeason', [String(season_number), series_title]) }); } catch (error) { return JSON.stringify({ success: false, message: `Error generating M3U for ${series_title} Season ${season_number}: ${error.message}` }); } } 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 "list_available_music_genres"() { if (!state.musicPlayer || !state.musicPlayer.isReady) { return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') }); } const allArtists = state.musicPlayer._generateFullArtistListForToken('all'); const genreSet = new Set(); allArtists.forEach(artist => { if (artist.genres && artist.genres.length > 0) { artist.genres.forEach(g => genreSet.add(g)); } }); const sortedGenres = Array.from(genreSet).sort((a, b) => a.localeCompare(b)); if (sortedGenres.length === 0) { return JSON.stringify({ success: true, genres: [], message: "No genres found in the user's library." }); } return JSON.stringify({ success: true, genres: sortedGenres }); } async "search_music_by_genre"({ genre_name }) { if (!state.musicPlayer || !state.musicPlayer.isReady) { return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') }); } const allArtists = state.musicPlayer._generateFullArtistListForToken('all'); const searchTerm = genre_name.toLowerCase().trim(); const matchingArtists = allArtists.filter(artist => artist.genres && artist.genres.some(g => g.toLowerCase().includes(searchTerm)) ); if (matchingArtists.length === 0) { return JSON.stringify({ success: false, message: _('aiToolSearchMusicByGenreNotFound', genre_name) }); } const artistNames = matchingArtists.map(a => a.title); switchView('music'); return JSON.stringify({ success: true, count: artistNames.length, artists: artistNames }); } 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: _('aiToolViewingHistoryCleared') }); } catch (error) { return JSON.stringify({ success: false, message: _('aiToolViewingHistoryClearError', 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 "view_history"() { switchView('history'); return JSON.stringify({ success: true, message: _('aiToolNavigatedToHistory') }); } async "view_favorites"() { switchView('favorites'); return JSON.stringify({ success: true, message: _('aiToolNavigatedToFavorites') }); } async "play_song"({ title, artist }) { if (!state.musicPlayer || !state.musicPlayer.isReady) { return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') }); } const song = await this.findSongLocally(title, artist); if (!song) { return JSON.stringify({ success: false, message: _('aiToolSongNotFound', title) }); } state.musicPlayer.showPlayer(); state.musicPlayer.cancionesActuales = [song]; state.musicPlayer.playSong(0); return JSON.stringify({ success: true, message: _('aiToolPlayingSong', [title, artist || _('unknownArtist')]) }); } async "toggle_light_mode"() { state.settings.theme = state.settings.theme === 'light' ? 'dark' : 'light'; await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]); applyTheme(state.settings.theme); return JSON.stringify({ success: true, message: state.settings.theme === 'light' ? _('aiToolLightModeOn') : _('aiToolLightModeOff') }); } async "toggle_hero_section"() { state.settings.showHero = !state.settings.showHero; await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]); applyHeroVisibility(state.settings.showHero); return JSON.stringify({ success: true, message: state.settings.showHero ? _('aiToolHeroOn') : _('aiToolHeroOff') }); } async "export_local_database"() { try { await exportDatabase(); return JSON.stringify({ success: true, message: _('aiToolDatabaseExported') }); } catch (error) { return JSON.stringify({ success: false, message: _('aiToolDatabaseExportError', error.message) }); } } async "import_local_database"() { showNotification(_('aiToolImportPrompt'), 'info', 5000); return JSON.stringify({ success: true, message: _('aiToolImportInitiated') }); } async "search_tmdb_content"({ query, type = 'all' }) { try { const searchEndpoint = type === 'all' ? `search/multi?query=${encodeURIComponent(query)}` : `search/${type}?query=${encodeURIComponent(query)}`; const searchResults = await fetchTMDB(searchEndpoint); if (!searchResults || !searchResults.results || searchResults.results.length === 0) { return JSON.stringify({ success: false, message: _('aiToolTmdbSearchNoResults', query) }); } const filteredResults = searchResults.results.filter(item => item.media_type !== 'person').slice(0, 10); if (filteredResults.length === 0) { return JSON.stringify({ success: false, message: _('aiToolTmdbSearchNoMoviesOrSeries', query) }); } renderGrid(filteredResults, false); switchView('search'); state.currentParams.query = query; return JSON.stringify({ success: true, message: _('aiToolTmdbSearchSuccess', [filteredResults.length, query]), results: filteredResults.map(i => i.title || i.name) }); } catch (error) { return JSON.stringify({ success: false, message: _('aiToolTmdbSearchError', error.message) }); } } async "get_trending_content"({ type = 'all' }) { try { const trendingEndpoint = `trending/${type}/day`; const trendingResults = await fetchTMDB(trendingEndpoint); if (!trendingResults || !trendingResults.results || trendingResults.results.length === 0) { return JSON.stringify({ success: false, message: _('aiToolTrendingNoResults') }); } renderGrid(trendingResults.results.filter(item => item.media_type !== 'person').slice(0, 10), false); switchView(type === 'movie' ? 'movies' : (type === 'tv' ? 'series' : 'home')); return JSON.stringify({ success: true, message: _('aiToolTrendingSuccess', trendingResults.results.length) }); } catch (error) { return JSON.stringify({ success: false, message: _('aiToolTrendingError', error.message) }); } } async "show_actor_details"({ actor_name }) { try { const searchResults = await fetchTMDB(`search/person?query=${encodeURIComponent(actor_name)}`); if (!searchResults || !searchResults.results || searchResults.results.length === 0) { return JSON.stringify({ success: false, message: _('aiToolActorNotFound', actor_name) }); } const actor = searchResults.results[0]; showActorDetails(actor.id); return JSON.stringify({ success: true, message: _('aiToolShowActorDetailsSuccess', actor.name) }); } catch (error) { return JSON.stringify({ success: false, message: _('aiToolShowActorDetailsError', error.message) }); } } async "play_trailer"({ title, type }) { try { const content = await this.findTmdbContent(title, type); if (!content) { return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) }); } const trailerKey = await getTrailerKey(content.id, content.type); if (!trailerKey) { return JSON.stringify({ success: false, message: _('aiToolTrailerNotFound') }); } showTrailer(trailerKey); return JSON.stringify({ success: true, message: _('aiToolPlayingTrailer', title) }); } catch (error) { return JSON.stringify({ success: false, message: _('aiToolPlayTrailerError', error.message) }); } } async "check_local_availability"({ title, type, year }) { const available = isContentAvailableLocally(title, type, year); return JSON.stringify({ success: true, available: available, message: available ? _('aiToolAvailableLocally', title) : _('aiToolNotAvailableLocally', title) }); } async "get_local_series_seasons"({ series_title, year }) { const seriesDetails = await this.findLocalSeriesDetails(series_title, year); if (seriesDetails.length === 0) { return JSON.stringify({ success: false, message: _('aiToolItemNotFound', series_title) }); } return JSON.stringify({ success: true, found: true, servers: seriesDetails }); } async "find_streaming_providers"({ title, type, year }) { const available = isContentAvailableLocally(title, type, year); if (available) { return JSON.stringify({ success: true, available: true, message: _('aiToolAvailableLocally', title) }); } try { const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}&year=${year || ''}`); if (!searchResults || !searchResults.results || searchResults.results.length === 0) { return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) }); } const item = searchResults.results[0]; const providersResult = await fetchTMDB(`${type}/${item.id}/watch/providers`); const region = state.settings.watchRegion || 'US'; const providers = providersResult.results[region]; if (!providers || !providers.flatrate) { return JSON.stringify({ success: false, available: false, message: _('aiToolNoStreamingProviders', title) }); } const providerNames = providers.flatrate.map(p => p.provider_name).join(', '); return JSON.stringify({ success: true, available: false, providers: providerNames, message: _('aiToolStreamingProvidersFound', [title, providerNames]) }); } catch (error) { return JSON.stringify({ success: false, message: _('aiToolStreamingProviderError', error.message) }); } } async findLocalContent(title, type) { const movieEntries = await getFromDB('movies'); const seriesEntries = await getFromDB('series'); const jellyfinMovieEntries = await getFromDB('jellyfin_movies'); const jellyfinSeriesEntries = await getFromDB('jellyfin_series'); const allContent = [ ...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))), ...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' }))), ...jellyfinMovieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))), ...jellyfinSeriesEntries.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 findLocalSeriesDetails(title, year) { const allSeriesSources = [...(await getFromDB('series')), ...(await getFromDB('jellyfin_series'))]; const normalizedTitle = title.toLowerCase().trim(); const serverDetails = []; for (const server of allSeriesSources) { if (server && server.titulos) { const foundSeries = server.titulos.find(s => s.title.toLowerCase().trim() === normalizedTitle && (!year || s.year == year) ); if (foundSeries) { const seasons = (foundSeries.seasons || []).map(season => ({ season_number: parseInt(season.index, 10), episode_count: parseInt(season.episodeCount, 10) })).sort((a, b) => a.season_number - b.season_number); serverDetails.push({ serverName: server.serverName || server.nombre || 'Servidor Desconocido', season_count: seasons.length, seasons: seasons }); } } } return serverDetails; } async findSongLocally(title, artist) { const allArtistsData = await getFromDB('artists'); for (const serverData of allArtistsData) { if (serverData && Array.isArray(serverData.titulos)) { for (const artistItem of serverData.titulos) { let songs; if (artistItem.isJellyfin) { songs = await state.musicPlayer.getArtistSongs({ id: artistItem.id, isJellyfin: true, serverUrl: artistItem.serverUrl, userId: artistItem.userId, token: artistItem.token }); } else { songs = await state.musicPlayer.getArtistSongs({ id: artistItem.id, isJellyfin: false, token: artistItem.token, protocolo: serverData.protocolo, ip: serverData.ip, puerto: serverData.puerto }); } const foundSong = songs.find(song => song.titulo.toLowerCase().includes(title.toLowerCase()) && (!artist || song.artista.toLowerCase().includes(artist.toLowerCase())) ); if (foundSong) return foundSong; } } } return null; } 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 = 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 }); } } }