CinePlex/js/ai-tools.js
2025-08-16 10:53:11 +02:00

994 lines
45 KiB
JavaScript

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 });
}
}
}