Asistente IA (openAI - proxyapi.ru)

This commit is contained in:
Filipinos 2025-07-29 15:05:06 +02:00
parent 9c7c57d41f
commit d6957d7959
15 changed files with 804 additions and 59 deletions

View File

@ -66,6 +66,7 @@
"settingsApiServer": { "message": "API- und Serverkonfiguration" },
"settingsTmdbApiLabel": { "message": "TMDB API-Schlüssel (Optional)" },
"settingsTmdbApiPlaceholder": { "message": "Verwendet den Standardschlüssel, wenn leer gelassen" },
"openaiApiKey": { "message": "OpenAI API-Schlüssel" },
"settingsTmdbLangLabel": { "message": "Sprache für TMDB & UI" },
"settingsRegionLabel": { "message": "Region für Anbieter" },
"allRegions": { "message": "Alle Regionen" },
@ -348,5 +349,17 @@
"m3uInstruction3": { "message": "Klicken Sie auf die Download-Schaltfläche." },
"m3uInstruction4": { "message": "Importieren Sie die .m3u-Datei in Ihren kompatiblen Player." },
"settingsRegionLabel": { "message": "Region für die Inhaltsentdeckung" },
"allRegions": { "message": "Alle Regionen" }
"allRegions": { "message": "Alle Regionen" },
"chatOpen": { "message": "Chat öffnen" },
"chatTitle": { "message": "KI-Assistent" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Nachricht eingeben..." },
"chatSend": { "message": "➤" },
"chatGptError": { "message": "Entschuldigung, ich konnte keine Antwort erhalten. Bitte versuchen Sie es erneut." },
"chatWelcome": { "message": "Willkommen! Ich bin Ihr CinePlex-Assistent. Fragen Sie mich nach Filmen, Serien oder allem, was Sie sonst noch wissen möchten." },
"chatApiKeyMissing": { "message": "Der OpenAI-API-Schlüssel ist nicht festgelegt. Bitte konfigurieren Sie ihn in den Erweiterungseinstellungen." },
"chatApiInvalidResponse": { "message": "Die API hat eine ungültige Antwort zurückgegeben. Bitte versuchen Sie es erneut." },
"chatApiError": { "message": "Fehler bei der Kommunikation mit dem KI-Assistenten" },
"downloadAll": { "message": "Alles herunterladen" },
"download": { "message": "Herunterladen" }
}

View File

@ -66,6 +66,7 @@
"settingsApiServer": { "message": "API and Server Configuration" },
"settingsTmdbApiLabel": { "message": "TMDB API Key (Optional)" },
"settingsTmdbApiPlaceholder": { "message": "Will use default key if left blank" },
"openaiApiKey": { "message": "OpenAI API Key" },
"settingsTmdbLangLabel": { "message": "Language for TMDB & UI" },
"settingsPhpUrlLabel": { "message": "Server URL for Adding Streams" },
"settingsPhpUrlPlaceholder": { "message": "https://your-server.com/path/to/script.php" },
@ -345,5 +346,17 @@
"m3uInstruction3": { "message": "Click the download button." },
"m3uInstruction4": { "message": "Import the .m3u file into your compatible player." },
"settingsRegionLabel": { "message": "Region for content discovery" },
"allRegions": { "message": "All regions" }
"allRegions": { "message": "All regions" },
"chatOpen": { "message": "Open Chat" },
"chatTitle": { "message": "AI Assistant" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Type your message..." },
"chatSend": { "message": "➤" },
"chatGptError": { "message": "Sorry, I couldn't get a response. Please try again." },
"chatWelcome": { "message": "Welcome! I'm your CinePlex assistant. Ask me about movies, series, or anything else you'd like to know." },
"chatApiKeyMissing": { "message": "OpenAI API key is not set. Please configure it in the extension settings." },
"chatApiInvalidResponse": { "message": "The API returned an invalid response. Please try again." },
"chatApiError": { "message": "Error communicating with the AI assistant" },
"downloadAll": { "message": "Download All" },
"download": { "message": "Download" }
}

View File

@ -66,6 +66,7 @@
"settingsApiServer": { "message": "Configuración de API y Servidor" },
"settingsTmdbApiLabel": { "message": "Clave de API de TMDB (Opcional)" },
"settingsTmdbApiPlaceholder": { "message": "Se usará la clave por defecto si se deja en blanco" },
"openaiApiKey": { "message": "Clave API de OpenAI" },
"settingsTmdbLangLabel": { "message": "Idioma para TMDB y la interfaz" },
"settingsPhpUrlLabel": { "message": "URL del Servidor para Añadir Streams" },
"settingsPhpUrlPlaceholder": { "message": "https://tu-servidor.com/ruta/al/script.php" },
@ -345,5 +346,17 @@
"m3uInstruction3": { "message": "Haz clic en el botón de descarga." },
"m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." },
"settingsRegionLabel": { "message": "Región para descubrimiento de contenido" },
"allRegions": { "message": "Todas las regiones" }
"allRegions": { "message": "Todas las regiones" },
"chatOpen": { "message": "Abrir Chat" },
"chatTitle": { "message": "Asistente IA" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Escribe tu mensaje..." },
"chatSend": { "message": "➤" },
"chatGptError": { "message": "Lo siento, no pude obtener una respuesta. Inténtalo de nuevo." },
"chatWelcome": { "message": "¡Bienvenido! Soy tu asistente de CinePlex. Pregúntame sobre películas, series o cualquier otra cosa que quieras saber." },
"chatApiKeyMissing": { "message": "La clave de la API de OpenAI no está configurada. Por favor, configúrala en los ajustes de la extensión." },
"chatApiInvalidResponse": { "message": "La API ha devuelto una respuesta no válida. Por favor, inténtalo de nuevo." },
"chatApiError": { "message": "Error al comunicarse con el asistente de IA" },
"downloadAll": { "message": "Descargar todo" },
"download": { "message": "Descargar" }
}

View File

@ -66,6 +66,7 @@
"settingsApiServer": { "message": "Configuration API et Serveur" },
"settingsTmdbApiLabel": { "message": "Clé API de TMDB (Optionnel)" },
"settingsTmdbApiPlaceholder": { "message": "Utilisera la clé par défaut si laissé vide" },
"openaiApiKey": { "message": "Clé API OpenAI" },
"settingsTmdbLangLabel": { "message": "Langue pour TMDB & UI" },
"settingsPhpUrlLabel": { "message": "URL du Serveur pour Ajout de Flux" },
"settingsPhpUrlPlaceholder": { "message": "https://votre-serveur.com/chemin/vers/script.php" },
@ -345,5 +346,17 @@
"m3uInstruction3": { "message": "Cliquez sur le bouton de téléchargement." },
"m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." },
"settingsRegionLabel": { "message": "Région pour la découverte de contenu" },
"allRegions": { "message": "Toutes les régions" }
"allRegions": { "message": "Toutes les régions" },
"chatOpen": { "message": "Ouvrir le chat" },
"chatTitle": { "message": "Assistant IA" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Écrivez votre message..." },
"chatSend": { "message": "➤" },
"chatGptError": { "message": "Désolé, je n'ai pas pu obtenir de réponse. Veuillez réessayer." },
"chatWelcome": { "message": "Bienvenue ! Je suis votre assistant CinePlex. Posez-moi des questions sur les films, les séries ou tout ce que vous voulez savoir." },
"chatApiKeyMissing": { "message": "La clé API OpenAI n'est pas définie. Veuillez la configurer dans les paramètres de l'extension." },
"chatApiInvalidResponse": { "message": "L'API a renvoyé une réponse non valide. Veuillez réessayer." },
"chatApiError": { "message": "Erreur de communication avec l'assistant IA" },
"downloadAll": { "message": "Tout télécharger" },
"download": { "message": "Télécharger" }
}

View File

@ -66,6 +66,7 @@
"settingsApiServer": { "message": "Configurazione API e Server" },
"settingsTmdbApiLabel": { "message": "Chiave API TMDB (Opzionale)" },
"settingsTmdbApiPlaceholder": { "message": "Verrà usata la chiave predefinita se lasciato vuoto" },
"openaiApiKey": { "message": "Chiave API OpenAI" },
"settingsTmdbLangLabel": { "message": "Lingua per TMDB e Interfaccia" },
"settingsPhpUrlLabel": { "message": "URL del server per aggiungere gli stream" },
"settingsPhpUrlPlaceholder": { "message": "https://tuo-server.com/percorso/dello/script.php" },
@ -345,5 +346,17 @@
"m3uInstruction3": { "message": "Clicca sul pulsante di download." },
"m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." },
"settingsRegionLabel": { "message": "Regione per la scoperta di contenuti" },
"allRegions": { "message": "Tutte le regioni" }
"allRegions": { "message": "Tutte le regioni" },
"chatOpen": { "message": "Apri chat" },
"chatTitle": { "message": "Assistente AI" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Scrivi il tuo messaggio..." },
"chatSend": { "message": "➤" },
"chatGptError": { "message": "Spiacenti, non sono riuscito a ottenere una risposta. Per favore, riprova." },
"chatWelcome": { "message": "Benvenuto! Sono il tuo assistente CinePlex. Chiedimi di film, serie o qualsiasi altra cosa tu voglia sapere." },
"chatApiKeyMissing": { "message": "La chiave API di OpenAI non è impostata. Configurala nelle impostazioni dell'estensione." },
"chatApiInvalidResponse": { "message": "L'API ha restituito una risposta non valida. Per favore, riprova." },
"chatApiError": { "message": "Errore di comunicazione con l'assistente AI" },
"downloadAll": { "message": "Scarica tutto" },
"download": { "message": "Scarica" }
}

View File

@ -66,6 +66,7 @@
"settingsApiServer": { "message": "Configuração de API e Servidor" },
"settingsTmdbApiLabel": { "message": "Chave da API do TMDB (Opcional)" },
"settingsTmdbApiPlaceholder": { "message": "Usará a chave padrão se deixado em branco" },
"openaiApiKey": { "message": "Chave da API OpenAI" },
"settingsTmdbLangLabel": { "message": "Idioma para TMDB e Interface" },
"settingsPhpUrlLabel": { "message": "URL do Servidor para Adicionar Streams" },
"settingsPhpUrlPlaceholder": { "message": "https://seu-servidor.com/caminho/para/script.php" },
@ -345,5 +346,17 @@
"m3uInstruction3": { "message": "Clique no botão de download." },
"m3uInstruction4": { "message": "Importe o arquivo .m3u para o seu reprodutor compatível." },
"settingsRegionLabel": { "message": "Região para descoberta de conteúdo" },
"allRegions": { "message": "Todas as regiões" }
"allRegions": { "message": "Todas as regiões" },
"chatOpen": { "message": "Abrir chat" },
"chatTitle": { "message": "Assistente de IA" },
"chatClose": { "message": "X" },
"chatPlaceholder": { "message": "Digite sua mensagem..." },
"chatSend": { "message": "➤" },
"chatGptError": { "message": "Desculpe, não consegui obter uma resposta. Por favor, tente novamente." },
"chatWelcome": { "message": "Bem-vindo! Eu sou seu assistente CinePlex. Pergunte-me sobre filmes, séries ou qualquer outra coisa que você queira saber." },
"chatApiKeyMissing": { "message": "A chave da API OpenAI não está definida. Configure-a nas configurações da extensão." },
"chatApiInvalidResponse": { "message": "A API retornou uma resposta inválida. Por favor, tente novamente." },
"chatApiError": { "message": "Erro ao se comunicar com o assistente de IA" },
"downloadAll": { "message": "Baixar tudo" },
"download": { "message": "Baixar" }
}

View File

@ -2936,9 +2936,7 @@ body.miniplayer-active #musicPlayerContainer {
}
.fab-btn {
position: fixed;
bottom: 2rem;
right: 2rem;
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
@ -3909,3 +3907,270 @@ body.miniplayer-active { padding-bottom: 85px; }
opacity: 1;
transform: translateY(0);
}
/* --- chat.css --- */
#fab-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1050;
display: flex;
flex-direction: row-reverse;
gap: 1rem;
align-items: flex-end;
transition: bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
body.miniplayer-active #fab-container {
bottom: calc(85px + 2rem);
}
.chat-fab, .fab-btn {
position: relative;
right: auto;
bottom: auto;
}
.chat-fab {
width: 60px;
height: 60px;
background: var(--gradient);
color: var(--primary);
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
cursor: pointer;
box-shadow: 0 5px 20px rgba(0, 224, 255, 0.3);
transition: var(--transition);
}
.chat-fab:hover {
transform: scale(1.1);
box-shadow: 0 8px 25px rgba(0, 224, 255, 0.4);
}
.chat-window {
position: fixed;
bottom: 95px;
right: 2rem;
width: 400px;
height: 520px;
max-width: 90vw;
max-height: 70vh;
background-color: var(--primary);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
border: 1px solid var(--glass-border);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 1051;
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
background: rgba(10, 10, 15, 0.8);
cursor: default;
}
.light-theme .chat-window {
background: rgba(244, 247, 250, 0.8);
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1.2rem;
background-color: rgba(255,255,255,0.05);
border-bottom: 1px solid var(--glass-border);
cursor: move;
flex-shrink: 0;
}
.chat-title {
font-family: 'Orbitron', sans-serif;
color: var(--text-primary);
font-size: 1.1rem;
margin: 0;
}
.chat-close-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.2rem;
cursor: pointer;
transition: var(--transition);
}
.chat-close-btn:hover {
color: var(--accent);
transform: rotate(90deg);
}
.chat-messages {
flex-grow: 1;
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
}
.chat-messages::-webkit-scrollbar {
width: 8px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background-color: var(--glass-border);
border-radius: 4px;
}
.message {
padding: 0.8rem 1.2rem;
border-radius: var(--border-radius-md);
max-width: 85%;
line-height: 1.6;
word-wrap: break-word;
}
.message p {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.message p:last-child {
margin-bottom: 0;
}
.user-message {
background: var(--gradient);
color: var(--primary);
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.assistant-message {
background-color: var(--secondary);
color: var(--text-primary);
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.chat-action-buttons {
display: flex;
flex-wrap: wrap;
gap: .5rem;
margin-top: .8rem;
padding-top: .8rem;
border-top: 1px solid var(--glass-border);
}
.chat-action-buttons button {
background-color: var(--glass);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: .4rem .8rem;
border-radius: 20px;
font-size: .8rem;
cursor: pointer;
transition: var(--transition);
}
.chat-action-buttons button:hover {
background-color: var(--accent);
color: var(--primary);
border-color: var(--accent);
}
.assistant-message code {
background-color: var(--primary);
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
border-radius: 6px;
font-family: monospace;
}
.typing-indicator {
display: flex;
gap: 5px;
}
.typing-indicator span {
width: 8px;
height: 8px;
background-color: var(--text-secondary);
border-radius: 50%;
animation: typing 1s infinite;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.chat-input-form {
display: flex;
padding: 0.8rem;
border-top: 1px solid var(--glass-border);
gap: 0.8rem;
background-color: rgba(255,255,255,0.05);
align-items: flex-end;
}
.chat-input {
flex-grow: 1;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 0.6rem 1rem;
color: var(--text-primary);
resize: none;
font-family: 'Montserrat', sans-serif;
font-size: 0.95rem;
}
.chat-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(0, 224, 255, 0.2);
}
.chat-send-btn {
flex-shrink: 0;
background: var(--accent);
color: var(--primary);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1rem;
cursor: pointer;
transition: var(--transition);
}
.chat-send-btn:hover {
background: var(--accent-dark);
transform: scale(1.1);
}
.chat-send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 768px) {
body.miniplayer-active #fab-container {
bottom: calc(110px + 1rem);
}
}
@media (max-width: 480px) {
.chat-window {
width: 100%;
height: 100%;
bottom: 0;
right: 0;
border-radius: 0;
max-height: none;
}
}

342
js/chat.js Normal file
View File

@ -0,0 +1,342 @@
import { state } from './state.js';
import { showNotification, _ } from './utils.js';
import { getFromDB } from './db.js';
import { showItemDetails, addStreamToList, downloadM3U } from './ui.js';
export class Chat {
constructor() {
this.dom = {
fab: document.getElementById('chat-fab'),
window: document.getElementById('chat-window'),
header: document.querySelector('.chat-header'),
closeBtn: document.getElementById('chat-close-btn'),
messagesContainer: document.getElementById('chat-messages'),
inputForm: document.getElementById('chat-input-form'),
input: document.getElementById('chat-input'),
sendBtn: document.getElementById('chat-send-btn')
};
this.isOpen = false;
this.isDragging = false;
this.offset = { x: 0, y: 0 };
this.conversationHistory = [];
this.bindEvents();
}
bindEvents() {
this.dom.fab.addEventListener('click', () => this.toggle());
this.dom.closeBtn.addEventListener('click', () => this.close());
this.dom.inputForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendMessage();
});
this.dom.input.addEventListener('input', this.autoResizeTextarea.bind(this));
this.dom.input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
this.dom.header.addEventListener('mousedown', this.startDrag.bind(this));
document.addEventListener('mousemove', this.drag.bind(this));
document.addEventListener('mouseup', this.stopDrag.bind(this));
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
if (this.isOpen) return;
this.isOpen = true;
this.dom.window.style.top = '';
this.dom.window.style.left = '';
this.dom.window.style.bottom = '95px';
this.dom.window.style.right = '2rem';
this.dom.window.style.display = 'flex';
gsap.fromTo(this.dom.window, { opacity: 0, scale: 0.9, y: 20 }, { opacity: 1, scale: 1, y: 0, duration: 0.3, ease: 'power3.out' });
gsap.to(this.dom.fab, { scale: 0, opacity: 0, duration: 0.2, ease: 'power2.in' });
if (this.conversationHistory.length === 0) {
this.addMessage(_('chatWelcome'), 'assistant');
this.conversationHistory.push({ role: 'assistant', content: _('chatWelcome') });
}
}
close() {
if (!this.isOpen) return;
this.isOpen = false;
gsap.to(this.dom.window, { opacity: 0, scale: 0.9, y: 20, duration: 0.3, ease: 'power2.in', onComplete: () => {
this.dom.window.style.display = 'none';
}});
gsap.fromTo(this.dom.fab, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)', delay: 0.2 });
}
async sendMessage() {
const userInput = this.dom.input.value.trim();
if (!userInput) return;
this.addMessage(userInput, 'user');
this.conversationHistory.push({ role: 'user', content: userInput });
this.dom.input.value = '';
this.autoResizeTextarea();
this.dom.sendBtn.disabled = true;
this.addTypingIndicator();
try {
const response = await this.getOpenAIResponse();
this.removeTypingIndicator();
this.addMessage(response, 'assistant');
this.conversationHistory.push({ role: 'assistant', content: response });
} catch (error) {
this.removeTypingIndicator();
this.addMessage(error.message, 'assistant', true);
} finally {
this.dom.sendBtn.disabled = false;
}
}
addMessage(text, sender, isError = false) {
const messageEl = document.createElement('div');
messageEl.classList.add('message', `${sender}-message`);
if(isError) messageEl.style.color = 'var(--danger)';
const p = document.createElement('p');
p.textContent = text;
messageEl.appendChild(p);
if (sender === 'assistant' && !isError) {
this.addMediaActionButtons(messageEl, text);
}
this.dom.messagesContainer.appendChild(messageEl);
this.scrollToBottom();
}
addTypingIndicator() {
const indicator = document.createElement('div');
indicator.id = 'typing-indicator';
indicator.classList.add('message', 'assistant-message', 'typing-indicator');
indicator.innerHTML = '<span></span><span></span><span></span>';
this.dom.messagesContainer.appendChild(indicator);
this.scrollToBottom();
}
removeTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) indicator.remove();
}
async getLocalContent() {
const movieEntries = await getFromDB('movies');
const seriesEntries = await getFromDB('series');
const processEntries = (entries, type) => {
return entries.flatMap(entry =>
(entry.titulos || []).map(titulo => ({ ...titulo, type }))
);
};
const movies = processEntries(movieEntries, 'movie');
const series = processEntries(seriesEntries, 'show');
return { movies, series };
}
async findLocalContent(title) {
const { movies, series } = await this.getLocalContent();
const searchTerm = title.trim().toLowerCase();
const allContent = [...movies, ...series];
let foundContent = allContent.find(m => m.title.toLowerCase() === searchTerm);
if (foundContent) return foundContent;
foundContent = allContent.find(m => m.title.toLowerCase().includes(searchTerm));
return foundContent;
}
async getOpenAIResponse() {
const apiKey = state.settings.openaiApiKey;
if (!apiKey) {
return _('chatApiKeyMissing');
}
const systemPrompt = `You are a helpful assistant for a movie and TV show enthusiast named CinePlex Assistant. Your main goal is to provide information about films, series, etc. When you identify a movie or series title in your response, you MUST enclose it in double quotes, for example: "The Matrix". Do not mention the user's local library or your ability to access it.`;
const messagesForApi = [
{ role: 'system', content: systemPrompt },
...this.conversationHistory
];
try {
const response = await fetch('https://api.proxyapi.ru/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: 'gpt-4o-2024-05-13',
messages: messagesForApi,
max_tokens: 250,
temperature: 0.7
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData?.error?.message || errorData?.error || `Error HTTP ${response.status}`;
console.error('Proxy API Error Response:', errorData);
throw new Error(errorMessage);
}
const data = await response.json();
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
return data.choices[0].message.content;
} else {
console.error('Invalid successful response format from proxy:', data);
throw new Error(_('chatApiInvalidResponse'));
}
} catch (error) {
console.error('OpenAI Proxy API call failed:', error);
showNotification(_('chatApiError'), 'error');
return _('chatApiError') + `: ${error.message}`;
}
}
autoResizeTextarea() {
this.dom.input.style.height = 'auto';
const scrollHeight = this.dom.input.scrollHeight;
if (scrollHeight > 200) {
this.dom.input.style.height = '200px';
this.dom.input.style.overflowY = 'auto';
} else {
this.dom.input.style.height = `${scrollHeight}px`;
this.dom.input.style.overflowY = 'hidden';
}
}
scrollToBottom() {
this.dom.messagesContainer.scrollTop = this.dom.messagesContainer.scrollHeight;
}
startDrag(e) {
if (e.target !== this.dom.header && e.target !== this.dom.header.querySelector('.chat-title')) return;
this.isDragging = true;
const rect = this.dom.window.getBoundingClientRect();
this.offset.x = e.clientX - rect.left;
this.offset.y = e.clientY - rect.top;
this.dom.header.style.cursor = 'grabbing';
}
drag(e) {
if (!this.isDragging) return;
e.preventDefault();
let newX = e.clientX - this.offset.x;
let newY = e.clientY - this.offset.y;
const winWidth = this.dom.window.offsetWidth;
const winHeight = this.dom.window.offsetHeight;
const docWidth = document.documentElement.clientWidth;
const docHeight = document.documentElement.clientHeight;
newX = Math.max(0, Math.min(newX, docWidth - winWidth));
newY = Math.max(0, Math.min(newY, docHeight - winHeight));
this.dom.window.style.left = `${newX}px`;
this.dom.window.style.top = `${newY}px`;
this.dom.window.style.bottom = 'auto';
this.dom.window.style.right = 'auto';
}
stopDrag() {
this.isDragging = false;
this.dom.header.style.cursor = 'move';
}
async addMediaActionButtons(messageEl, text) {
const contentTitles = this.extractContentTitles(text);
if (contentTitles.length === 0) return;
const allContent = [];
for (const title of contentTitles) {
const content = await this.findLocalContent(title);
if (content) {
allContent.push(content);
}
}
if (allContent.length > 0) {
allContent.forEach(content => {
const itemContainer = document.createElement('div');
itemContainer.className = 'chat-item-actions';
const titleEl = document.createElement('span');
titleEl.className = 'chat-action-title';
titleEl.textContent = content.title;
itemContainer.appendChild(titleEl);
const buttonContainer = document.createElement('div');
buttonContainer.className = 'chat-action-buttons';
const buttons = [
{ label: _('moreInfo'), action: 'info', icon: '' },
{ label: _('addStream'), action: 'stream', icon: '▶️' },
{ label: _('download'), action: 'download', icon: '⬇️' }
];
buttons.forEach(btnInfo => {
const button = document.createElement('button');
button.innerHTML = `${btnInfo.icon} ${btnInfo.label}`;
button.addEventListener('click', () => this.handleMediaAction(btnInfo.action, content, button));
buttonContainer.appendChild(button);
});
itemContainer.appendChild(buttonContainer);
messageEl.appendChild(itemContainer);
});
if (allContent.length > 1) {
const downloadAllButtonContainer = document.createElement('div');
downloadAllButtonContainer.className = 'chat-action-buttons chat-download-all';
const downloadAllButton = document.createElement('button');
downloadAllButton.innerHTML = `⬇️ ${_('downloadAll')}`;
downloadAllButton.addEventListener('click', (e) => {
const streams = allContent.map(content => ({ title: content.title, type: content.type }));
downloadM3U(streams, e.target);
});
downloadAllButtonContainer.appendChild(downloadAllButton);
messageEl.appendChild(downloadAllButtonContainer);
}
}
}
extractContentTitles(text) {
const matches = text.match(/"([^"]+)"/g);
if (!matches) return [];
return matches.map(match => match.substring(1, match.length - 1));
}
handleMediaAction(action, content, buttonEl) {
switch (action) {
case 'info':
showItemDetails(Number(content.id), content.type);
this.close();
break;
case 'stream':
addStreamToList(content.title, content.type, buttonEl);
break;
case 'download':
downloadM3U(content.title, content.type, buttonEl);
break;
default:
showNotification(`Acción desconocida: ${action}`, 'warning');
}
}
}

View File

@ -2,4 +2,5 @@ export const config = {
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
dbName: 'PlexDB',
dbVersion: 9,
defaultOpenaiApiKey: 'sk-r5HgSL5o102oGaXEuF13P5sNqpLtDVDq',
};

View File

@ -3,6 +3,7 @@ import { config } from './config.js';
import { initDB, getFromDB } from './db.js';
import { MusicPlayer } from './musicPlayer.js';
import { ActivityViewer } from './activityViewer.js';
import { Chat } from './chat.js';
import { setupEventListeners } from './eventListeners.js';
import { fetchAllProviders, renderProviders, getRegions } from './providers.js';
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
@ -15,12 +16,10 @@ async function loadSettings() {
state.settings = { ...state.settings, ...settingsData[0] };
}
// Ensure a default region is set if none exists
if (!state.settings.watchRegion) {
state.settings.watchRegion = 'US';
}
// Ensure language is always set, fallback to UI language
if (!state.settings.language) {
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
}
@ -36,7 +35,6 @@ async function loadSettings() {
} catch (error) {
console.error("Could not load settings from DB, using defaults.", error);
// Fallback to defaults in case of any error
state.settings.watchRegion = 'US';
}
}
@ -54,7 +52,7 @@ document.addEventListener('DOMContentLoaded', async () => {
state.musicPlayer = new MusicPlayer();
state.musicPlayer.setDB(state.db);
state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal'));
state.chat = new Chat();
initializeFavorites();
initializeUserData();

View File

@ -0,0 +1,59 @@
import { showNotification, _ } from './utils.js';
export const phpScriptGenerator = (() => {
let dom = {};
function cacheDom() {
const settingsModal = document.getElementById('settingsModal');
if (!settingsModal) return false;
dom.secretKeyCheck = settingsModal.querySelector('#phpSecretKeyCheck');
dom.secretKey = settingsModal.querySelector('#phpSecretKey');
dom.savePath = settingsModal.querySelector('#phpSavePath');
dom.filename = settingsModal.querySelector('#phpFilename');
dom.fileActionAppendRadio = settingsModal.querySelector('#phpFileActionAppend');
dom.generatedCode = settingsModal.querySelector('#generatedPhpCode');
dom.generateBtn = settingsModal.querySelector('#generatePhpScriptBtn');
dom.copyBtn = settingsModal.querySelector('#copyPhpScriptBtn');
return dom.generateBtn && dom.copyBtn;
}
function init() {
if (!cacheDom()) {
return;
}
dom.generateBtn.addEventListener('click', generatePhpScript);
dom.copyBtn.addEventListener('click', copyScript);
}
function generatePhpScript() {
const useSecretKey = dom.secretKeyCheck.checked;
const secretKey = dom.secretKey.value.trim();
const savePath = dom.savePath.value.trim();
const filename = dom.filename.value.trim() || 'CinePlex_Playlist.m3u';
const appendToFile = dom.fileActionAppendRadio.checked;
let script = `<?php\nheader('Content-Type: application/json');\nheader('Access-Control-Allow-Origin: *');\nheader('Access-Control-Allow-Methods: POST, OPTIONS');\nheader('Access-Control-Allow-Headers: Content-Type, Origin, X-Secret-Key');\n\nif ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {\n http_response_code(200);\n exit(0);\n}\n\ndefine('SAVE_DIRECTORY', '${savePath.replace(/'/g, "\\'")}');\ndefine('FILENAME', '${filename.replace(/'/g, "\\'")}');\ndefine('FILE_ACTION_APPEND', ${appendToFile ? 'true' : 'false'});\n${useSecretKey ? `define('SECRET_KEY', '${secretKey.replace(/'/g, "\\'")}');` : ''}\n\nfunction sendResponse($success, $message, $filename = '', $http_code = 200) {\n if (!$success && $http_code === 200) {\n $http_code = 400;\n }\n http_response_code($http_code);\n echo json_encode(['success' => $success, 'message' => $message, 'filename' => $filename]);\n exit;\n}\n`;
if (useSecretKey) {
script += `\n$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';\nif (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {\n sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);\n}\n`;
}
script += `\n$json_data = file_get_contents('php://input');\n$data = json_decode($json_data, true);\n\nif (json_last_error() !== JSON_ERROR_NONE) {\n sendResponse(false, 'Error: Datos JSON inválidos.');\n}\n\nif (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {\n sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');\n}\n\n$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\') : __DIR__;\n\nif (!is_dir($save_dir) || !is_writable($save_dir)) {\n sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);\n}\n\n$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));\n$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);\n$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;\n\n$content_to_write = "";\n\nif (FILE_ACTION_APPEND) {\n $file_exists = file_exists($target_path);\n if (!$file_exists) {\n $content_to_write .= "#EXTM3U\\n";\n }\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {\n sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);\n }\n} else { // Overwrite mode\n $content_to_write = "#EXTM3U\\n";\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {\n sendResponse(true, 'Archivo de streams sobrescrito correctamente.', $safe_filename, 201);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo escribir el archivo.', '', 500);\n }\n}\n?>`;
dom.generatedCode.value = script;
showNotification(_("scriptGenerated"), "success");
}
function copyScript() {
if (!dom.generatedCode.value || dom.generatedCode.value.trim() === '') {
showNotification(_("errorGeneratingScript"), "warning");
return;
}
navigator.clipboard.writeText(dom.generatedCode.value).then(() => {
showNotification(_("scriptCopied"), "success");
}).catch(err => {
showNotification(_("errorCopyingScript"), "error");
});
}
return { init };
})();

View File

@ -87,9 +87,11 @@ async function parseAndStoreSectionItems(contentXml, storeName, serverData) {
if (type === 'movie' || type === 'show') {
const itemSelector = type === 'movie' ? 'Video' : 'Directory';
items = Array.from(contentXml.querySelectorAll(itemSelector)).map(el => ({
id: el.getAttribute('ratingKey'),
title: el.getAttribute('title'),
year: el.getAttribute('year'),
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre')
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre'),
type: type
}));
} else if (type === 'artist' || type === 'photo') {
items = Array.from(contentXml.querySelectorAll('Directory')).map(el => ({

View File

@ -1,3 +1,5 @@
import { config } from './config.js';
export const state = {
currentPage: 1,
currentView: 'movies',
@ -5,6 +7,7 @@ export const state = {
settings: {
id: 'user_settings',
apiKey: '',
openaiApiKey: config.defaultOpenaiApiKey || '',
theme: 'dark',
showHero: true,
language: 'es',
@ -46,6 +49,7 @@ export const state = {
isScanningJellyfin: false,
musicPlayer: null,
activityViewer: null,
chat: null,
currentContentFetchController: null,
plexScanAbortController: null,
aceEditor: null,

View File

@ -1462,48 +1462,7 @@ export async function addStreamToList(title, type, buttonElement = null) {
}
}
export async function downloadM3U(title, type, buttonElement = null) {
if (state.isDownloadingM3U) return;
state.isDownloadingM3U = true;
let originalButtonContent = null;
if (buttonElement) {
originalButtonContent = buttonElement.innerHTML;
buttonElement.disabled = true;
buttonElement.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`;
}
showNotification(_('generatingM3U', title), "info");
try {
const streamData = await fetchAllAvailableStreams(title, type);
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
let m3uContent = "#EXTM3U\n";
streamData.streams.forEach(stream => {
m3uContent += `${stream.extinf}\n${stream.url}\n`;
});
const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title.replace(/[^a-z0-9]/gi, '_')}.m3u`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification(_('m3uDownloaded', title), 'success');
} catch (error) {
showNotification(_('errorGeneratingM3U', error.message), "error");
} finally {
state.isDownloadingM3U = false;
if (buttonElement && originalButtonContent) {
buttonElement.innerHTML = originalButtonContent;
buttonElement.disabled = false;
}
}
}
export async function downloadM3U(items, buttonElement = null) { if (state.isDownloadingM3U) return; state.isDownloadingM3U = true; let originalButtonContent = null; if (buttonElement) { originalButtonContent = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`; } const itemsArray = Array.isArray(items) ? items : [{ title: items, type: arguments[1] }]; const collectiveTitle = itemsArray.length > 1 ? 'CinePlex_Playlist' : itemsArray[0].title; showNotification(_('generatingM3U', collectiveTitle), "info"); try { let m3uContent = "#EXTM3U\n"; let streamsFound = 0; for (const item of itemsArray) { try { const streamData = await fetchAllAvailableStreams(item.title, item.type); if (streamData.success && streamData.streams.length > 0) { streamsFound += streamData.streams.length; streamData.streams.forEach(stream => { m3uContent += `${stream.extinf}\n${stream.url}\n`; }); } } catch (error) { console.warn(`Could not fetch streams for ${item.title}:`, error); } } if (streamsFound === 0) throw new Error(_('noStreamsFoundForSelection')); const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${collectiveTitle.replace(/[^a-z0-9]/gi, '_')}.m3u`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification(_('m3uDownloaded', collectiveTitle), 'success'); } catch (error) { showNotification(_('errorGeneratingM3U', error.message), "error"); } finally { state.isDownloadingM3U = false; if (buttonElement && originalButtonContent) { buttonElement.innerHTML = originalButtonContent; buttonElement.disabled = false; } }}
export function showTrailer(key) {
const lightbox = document.getElementById('video-lightbox');
@ -1563,6 +1522,7 @@ export function activateSettingsTab(tabId) {
export function openSettingsModal() {
document.getElementById('tmdbApiKey').value = state.settings.apiKey;
document.getElementById('openaiApiKey').value = state.settings.openaiApiKey || '';
document.getElementById('phpScriptUrl').value = state.settings.phpScriptUrl || '';
document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
document.getElementById('showHeroToggle').checked = state.settings.showHero;
@ -1589,6 +1549,7 @@ export async function saveSettings() {
const newSettings = {
id: 'user_settings',
apiKey: document.getElementById('tmdbApiKey').value.trim(),
openaiApiKey: document.getElementById('openaiApiKey').value.trim(),
theme: document.getElementById('lightModeToggle').checked ? 'light' : 'dark',
showHero: document.getElementById('showHeroToggle').checked,
phpScriptUrl: document.getElementById('phpScriptUrl').value.trim(),

View File

@ -331,8 +331,6 @@
</div>
</div>
<button id="fab-music-player" class="fab-btn" style="display: none;" title="__MSG_openMusicPlayer__"><i class="fas fa-music"></i></button>
<div class="spinner" id="spinner"></div>
<div class="modal fade" id="activityViewerModal" tabindex="-1" aria-labelledby="activityViewerModalLabel" aria-hidden="true" role="dialog" aria-modal="true">
@ -400,6 +398,11 @@
<label for="tmdbApiKey" class="form-label">__MSG_settingsTmdbApiLabel__</label>
<input type="password" class="form-control" id="tmdbApiKey" placeholder="__MSG_settingsTmdbApiPlaceholder__">
</div>
<div class="mb-3">
<label for="openaiApiKey" class="form-label">__MSG_settingsOpenAiApiLabel__</label>
<label for="openaiApiKey" class="form-label">__MSG_openaiApiKey__</label>
<input type="password" class="form-control" id="openaiApiKey" placeholder="__MSG_settingsOpenAiApiPlaceholder__">
</div>
<div class="mb-3">
<label for="region-filter" class="form-label">__MSG_settingsRegionLabel__</label>
<select class="form-control filter-select" id="region-filter">
@ -742,6 +745,37 @@
</div>
</div>
<div id="fab-container">
<button id="fab-music-player" class="fab-btn" style="display: none;" title="__MSG_openMusicPlayer__"><i class="fas fa-music"></i></button>
<div id="chat-fab" class="chat-fab" data-i18n-title="chatOpen" title="Open Chat">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="28" height="28">
<path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H5.2L4 17.2V4H20V16Z"/>
<path d="M7.5 8.5h9v2h-9zM7.5 11.5h6v2h-6z"/>
</svg>
</div>
</div>
<div id="chat-window" class="chat-window" style="display: none;">
<div class="chat-header">
<h3 class="chat-title" data-i18n="chatTitle">AI Assistant</h3>
<button id="chat-close-btn" class="chat-close-btn" data-i18n-title="chatClose" title="Close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
</div>
<div id="chat-messages" class="chat-messages">
</div>
<form id="chat-input-form" class="chat-input-container">
<textarea id="chat-input" class="chat-input" data-i18n-placeholder="chatPlaceholder" placeholder="Type your message..." rows="1"></textarea>
<button id="chat-send-btn" type="submit" class="chat-send-btn" data-i18n-title="chatSend" title="Send">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</form>
</div>
<script src="lib/bootstrap.bundle.min.js"></script>
<script src="lib/gsap.min.js"></script>
<script src="lib/ScrollTrigger.min.js"></script>
@ -752,6 +786,7 @@
<script type="module" src="js/main.js"></script>
<script type="module" src="js/activityViewer.js"></script>
<script type="module" src="js/m3u-generator.js"></script>
<script type="module" src="js/chat.js"></script>
</body>