521 lines
22 KiB
JavaScript
521 lines
22 KiB
JavaScript
import { state } from './state.js';
|
|
import { getFromDB, clearStore, addItemsToStore } from './db.js';
|
|
import { showNotification, _ } from './utils.js';
|
|
import { switchView, showItemDetails, addStreamToList, downloadM3U, generateStatistics, toggleFavorite, loadRecommendations, applyFilters as applyUIFilters, clearAllFavorites, clearRecommendations, loadInitialContent, loadContent, clearAllHistory } from './ui.js';
|
|
import { fetchTMDB } from './api.js';
|
|
import { updateAllTokens, addPlexToken } from './plex.js';
|
|
import { config } from './config.js';
|
|
|
|
export class AITools {
|
|
constructor(chatInstance) {
|
|
this.chat = chatInstance;
|
|
this.genreCache = { movie: null, tv: null };
|
|
}
|
|
|
|
get toolDefinitions() {
|
|
return [
|
|
{
|
|
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'],
|
|
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: '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: '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: {} }
|
|
}
|
|
];
|
|
}
|
|
|
|
async "search_library"({ query, type, resolution, container }) {
|
|
const movieEntries = await getFromDB('movies');
|
|
const seriesEntries = await getFromDB('series');
|
|
const allContent = [
|
|
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
|
|
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
|
|
];
|
|
|
|
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 movieItems = (await getFromDB('movies')).flatMap(s => s.titulos);
|
|
const seriesItems = (await getFromDB('series')).flatMap(s => s.titulos);
|
|
const artistItems = (await getFromDB('artists')).flatMap(s => s.titulos);
|
|
const stats = {
|
|
totalMovies: new Set(movieItems.map(item => item.title)).size,
|
|
totalSeries: new Set(seriesItems.map(item => item.title)).size,
|
|
totalArtists: new Set(artistItems.map(item => item.title)).size,
|
|
};
|
|
switchView('stats');
|
|
return JSON.stringify({ success: true, stats });
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: _('aiToolStatsError') });
|
|
}
|
|
}
|
|
|
|
async "show_item_details"({ title, type }) {
|
|
const content = await this.findTmdbContent(title, type);
|
|
if (!content) {
|
|
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
|
|
}
|
|
showItemDetails(Number(content.id), content.type);
|
|
return JSON.stringify({ success: true, message: _('aiToolShowItemDetailsSuccess', title) });
|
|
}
|
|
|
|
async "add_to_playlist"({ title, type }) {
|
|
const content = await this.findLocalContent(title, type);
|
|
if (!content) {
|
|
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
|
|
}
|
|
addStreamToList(content.title, content.type);
|
|
return JSON.stringify({ success: true, message: _('aiToolAddToPlaylistSuccess', title) });
|
|
}
|
|
|
|
async "check_and_download_titles_list"({ titles, type, filename }) {
|
|
try {
|
|
if (!titles || titles.length === 0) {
|
|
return JSON.stringify({ success: false, message: _('aiToolM3UNoTitlesProvided') });
|
|
}
|
|
|
|
showNotification(_('aiToolM3UCheckingTitles'), 'info');
|
|
|
|
const plexStore = type === 'movie' ? 'movies' : 'series';
|
|
const jellyfinStore = type === 'movie' ? 'jellyfin_movies' : 'jellyfin_series';
|
|
|
|
const plexContent = await getFromDB(plexStore);
|
|
const jellyfinContent = await getFromDB(jellyfinStore);
|
|
|
|
const allLocalItems = [
|
|
...plexContent.flatMap(server => server.titulos || []),
|
|
...jellyfinContent.flatMap(lib => lib.titulos || [])
|
|
];
|
|
|
|
const normalize = (str) => str ? str.toLowerCase().trim().replace(/[^a-z0-9]/g, '') : '';
|
|
const normalizedTitlesFromAI = new Set(titles.map(normalize));
|
|
|
|
const matchedItems = [];
|
|
const seenTitles = new Set();
|
|
|
|
for (const localItem of allLocalItems) {
|
|
const normalizedLocalTitle = normalize(localItem.title);
|
|
if (normalizedTitlesFromAI.has(normalizedLocalTitle) && !seenTitles.has(normalizedLocalTitle)) {
|
|
matchedItems.push({ title: localItem.title, type });
|
|
seenTitles.add(normalizedLocalTitle);
|
|
}
|
|
}
|
|
|
|
if (matchedItems.length === 0) {
|
|
return JSON.stringify({ success: true, message: _('aiToolM3UNoLocalMatchesForDownload') });
|
|
}
|
|
|
|
downloadM3U(matchedItems, null, filename);
|
|
|
|
return JSON.stringify({ success: true, message: _('aiToolM3UDownloadStarted', [String(matchedItems.length), String(titles.length)]) });
|
|
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: `Error creating playlist: ${error.message}` });
|
|
}
|
|
}
|
|
|
|
async "toggle_favorite"({ title, type }) {
|
|
const item = await this.findTmdbContent(title, type);
|
|
if (!item) {
|
|
return JSON.stringify({ success: false, message: _('aiToolItemNotFound', title) });
|
|
}
|
|
toggleFavorite(item.id, type);
|
|
const isFavorite = state.favorites.some(fav => fav.id === item.id && fav.type === type);
|
|
const message = isFavorite ? _('aiToolFavoriteAdded', title) : _('aiToolFavoriteRemoved', title);
|
|
return JSON.stringify({ success: true, message });
|
|
}
|
|
|
|
async "get_recommendations"() {
|
|
switchView('recommendations');
|
|
return JSON.stringify({ success: true, message: _('aiToolRecommendationsSuccess') });
|
|
}
|
|
|
|
async "apply_filters"({ type, genre, year, sort }) {
|
|
switchView(type === 'movie' ? 'movies' : 'series');
|
|
let genreId = '';
|
|
if (genre) {
|
|
genreId = await this.getGenreId(genre, type);
|
|
if (!genreId) {
|
|
return JSON.stringify({ success: false, message: _('aiToolApplyFiltersGenreNotFound', genre) });
|
|
}
|
|
}
|
|
state.currentParams.genre = genreId;
|
|
state.currentParams.year = year || '';
|
|
state.currentParams.sort = sort || 'popularity.desc';
|
|
applyUIFilters();
|
|
return JSON.stringify({ success: true, message: _('aiToolApplyFiltersSuccess') });
|
|
}
|
|
|
|
async "play_music_by_artist"({ artist_name }) {
|
|
if (!state.musicPlayer || !state.musicPlayer.isReady) {
|
|
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNotReady') });
|
|
}
|
|
const allArtists = state.musicPlayer._generateFullArtistListForToken('all');
|
|
const searchTerm = artist_name.toLowerCase().trim();
|
|
const artist = allArtists.find(a => a.title.toLowerCase() === searchTerm);
|
|
if (!artist) {
|
|
return JSON.stringify({ success: false, message: _('aiToolPlayMusicArtistNotFound', artist_name) });
|
|
}
|
|
state.musicPlayer.showPlayer();
|
|
const songs = await state.musicPlayer.getArtistSongs(artist);
|
|
if (!songs || songs.length === 0) {
|
|
return JSON.stringify({ success: false, message: _('aiToolPlayMusicNoSongs', artist_name) });
|
|
}
|
|
state.musicPlayer.cancionesActuales = songs;
|
|
state.musicPlayer.playSong(0);
|
|
return JSON.stringify({ success: true, message: _('aiToolPlayMusicSuccess', artist_name) });
|
|
}
|
|
|
|
async "clear_chat_history"() {
|
|
try {
|
|
this.chat.clearHistory();
|
|
return JSON.stringify({ success: true, message: _('aiToolChatHistoryCleared') });
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: _('aiToolChatHistoryClearError', error.message) });
|
|
}
|
|
}
|
|
|
|
async "delete_database"() {
|
|
try {
|
|
if (confirm(_('aiToolConfirmDeleteDatabase'))) {
|
|
if (state.db) {
|
|
state.db.close();
|
|
}
|
|
const deleteRequest = indexedDB.deleteDatabase(config.dbName);
|
|
return new Promise((resolve) => {
|
|
deleteRequest.onsuccess = () => {
|
|
setTimeout(() => window.location.reload(), 1500);
|
|
resolve(JSON.stringify({ success: true, message: _('aiToolDatabaseDeleted') }));
|
|
};
|
|
deleteRequest.onerror = (event) => {
|
|
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', event.target.error) }));
|
|
};
|
|
deleteRequest.onblocked = () => {
|
|
resolve(JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteBlocked') }));
|
|
};
|
|
});
|
|
} else {
|
|
return JSON.stringify({ success: false, message: _('aiToolDeleteDatabaseCancelled') });
|
|
}
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: _('aiToolDatabaseDeleteError', error.message) });
|
|
}
|
|
}
|
|
|
|
async "update_all_tokens"() {
|
|
try {
|
|
await updateAllTokens();
|
|
return JSON.stringify({ success: true, message: _('aiToolUpdateAllTokensSuccess') });
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: _('aiToolUpdateAllTokensError', error.message) });
|
|
}
|
|
}
|
|
|
|
async "add_plex_token"({ token }) {
|
|
try {
|
|
await addPlexToken(token);
|
|
return JSON.stringify({ success: true, message: _('aiToolAddPlexTokenSuccess') });
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: _('aiToolAddPlexTokenError', error.message) });
|
|
}
|
|
}
|
|
|
|
async "change_region"({ region }) {
|
|
try {
|
|
state.settings.watchRegion = region;
|
|
await addItemsToStore('settings', [{ id: 'user_settings', ...state.settings }]);
|
|
await loadInitialContent();
|
|
if (['movies', 'series', 'search'].includes(state.currentView)) {
|
|
await loadContent();
|
|
}
|
|
return JSON.stringify({ success: true, message: _('aiToolChangeRegionSuccess', region) });
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: _('aiToolChangeRegionError', error.message) });
|
|
}
|
|
}
|
|
|
|
async "clear_all_favorites"() {
|
|
try {
|
|
clearAllFavorites();
|
|
return JSON.stringify({ success: true, message: _('aiToolFavoritesCleared') });
|
|
} catch (error) {
|
|
return JSON.stringify({ success: false, message: _('aiToolFavoritesClearError', error.message) });
|
|
}
|
|
}
|
|
|
|
async "clear_viewing_history"() {
|
|
try {
|
|
clearAllHistory();
|
|
return JSON.stringify({ success: true, message: _('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 findLocalContent(title, type) {
|
|
const movieEntries = await getFromDB('movies');
|
|
const seriesEntries = await getFromDB('series');
|
|
const allContent = [
|
|
...movieEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'movie' }))),
|
|
...seriesEntries.flatMap(e => (e.titulos || []).map(t => ({ ...t, type: 'series' })))
|
|
];
|
|
const searchTerm = title.toLowerCase().trim();
|
|
return allContent.find(item => item.title.toLowerCase() === searchTerm && (!type || item.type === type));
|
|
}
|
|
|
|
async findTmdbContent(title, type) {
|
|
try {
|
|
const searchResults = await fetchTMDB(`search/${type}?query=${encodeURIComponent(title)}`);
|
|
if (searchResults && searchResults.results.length > 0) {
|
|
const item = searchResults.results[0];
|
|
return { id: item.id, type };
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getGenreId(genreName, type) {
|
|
if (!this.genreCache[type]) {
|
|
try {
|
|
const data = await fetchTMDB(`genre/${type}/list`);
|
|
this.genreCache[type] = data.genres;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
const genre = this.genreCache[type].find(g => g.name.toLowerCase() === genreName.toLowerCase());
|
|
return genre ? genre.id : null;
|
|
}
|
|
|
|
async executeTool(toolCall) {
|
|
const functionName = toolCall.function.name;
|
|
const args = 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 });
|
|
}
|
|
}
|
|
} |