994 lines
45 KiB
JavaScript
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 });
|
|
}
|
|
}
|
|
} |