# monitor_manager.py
import tkinter as tk
import psutil
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import threading
import time
import platform
import datetime
import csv
from tkinter import messagebox, ttk
import requests
from bs4 import BeautifulSoup
import re
# Importaciones directas de módulos (Acceso con el prefijo del módulo)
import config
import system_utils
# ===============================================
# Lógica del Panel Lateral (Resumen Rápido)
# ===============================================
def actualizar_resumen_lateral(root):
"""Hilo que actualiza la información básica del sistema."""
boot_time_timestamp = psutil.boot_time()
while config.monitor_running:
try:
# 1. Hostname, OS, Uptime...
hostname_str = platform.node()
os_name = platform.system()
os_version = platform.release()
arch = platform.machine()
os_str = f"{os_name} {os_version} ({arch})"
current_time = time.time()
uptime_seconds = int(current_time - boot_time_timestamp)
uptime_delta = str(datetime.timedelta(seconds=uptime_seconds))
# Chequeo antes de llamar a root.after
if root.winfo_exists():
root.after(0, config.label_hostname.config, {"text": f"Host: {hostname_str}"})
root.after(0, config.label_os_info.config, {"text": f"OS: {os_str}"})
root.after(0, config.label_uptime.config, {"text": f"Uptime: {uptime_delta.split('.')[0]}"})
else:
break # Salir si la ventana ya no existe
except Exception as e:
if root.winfo_exists():
root.after(0, system_utils.log_event, f"Error en hilo de resumen lateral: {e}")
time.sleep(5) # Actualizar cada 5 segundos
def crear_panel_lateral(frame, root):
"""Crea el panel lateral izquierdo SOLO con el resumen rápido."""
# --- Sección de Resumen del Sistema ---
resumen_frame = tk.LabelFrame(frame, text="Resumen Rápido", padx=10, pady=10)
resumen_frame.pack(fill="x", padx=10, pady=10)
label_style = {'font': ('Helvetica', 9, 'bold'), 'anchor': 'w', 'bg': frame['bg']}
config.label_hostname = tk.Label(resumen_frame, text="Host: Cargando...", **label_style)
config.label_hostname.pack(fill="x", pady=2)
config.label_os_info = tk.Label(resumen_frame, text="OS: Cargando...", **label_style)
config.label_os_info.pack(fill="x", pady=2)
config.label_uptime = tk.Label(resumen_frame, text="Uptime: Cargando...", **label_style)
config.label_uptime.pack(fill="x", pady=2)
# Iniciar el hilo de actualización del resumen
summary_thread = threading.Thread(target=lambda: actualizar_resumen_lateral(root))
summary_thread.daemon = True
summary_thread.start()
# ===============================================
# Lógica de Web Scraping
# ===============================================
def scrappear_pagina_principal(url, tipo_extraccion, output_text_widget, progress_bar, selector, atributo, config_data, root):
"""
Realiza la extracción de datos de la URL. Ahora acepta selector, atributo y config_data.
Se ha eliminado el límite de elementos para los modos avanzados y combinados.
"""
# 1. Validación y Control
if config.scraping_running:
root.after(0, system_utils.log_event, "Ya hay una extracción en curso. Detenla primero.")
return
config.scraping_running = True
# Si hay configuración JSON cargada, se usan esos datos
if config_data:
try:
# Si el JSON contiene URL, se usa, sino se usa la de la interfaz (ya actualizada por system_utils)
if 'url' in config_data:
url = config_data.get('url')
tipo_extraccion = config_data.get('type', tipo_extraccion)
selector = config_data.get('selector', selector)
atributo = config_data.get('attribute', atributo)
root.after(0, system_utils.log_event, f"Usando configuración JSON: Tipo={tipo_extraccion}, Selector={selector}")
except Exception as e:
root.after(0, system_utils.log_event, f"ERROR al leer config JSON: {e}")
config.scraping_running = False
return
# Validación específica para modos avanzados
is_advanced = tipo_extraccion in ["-> Texto Específico (CSS Selector)", "-> Atributo Específico (CSS Selector + Attr)", "Portátiles Gamer (Enlace + Precio)"]
if is_advanced and not selector and tipo_extraccion != "Portátiles Gamer (Enlace + Precio)":
root.after(0, system_utils.log_event, "ERROR: El modo avanzado requiere un Selector CSS/Tag.")
root.after(0, lambda: progress_bar.stop())
config.scraping_running = False
return
def perform_scraping():
if not root.winfo_exists() or not config.scraping_running:
config.scraping_running = False
return
# 2. Preparar UI (hilo principal)
root.after(0, progress_bar.start, 10)
root.after(0, system_utils.log_event, f"Iniciando extracción de '{tipo_extraccion}' en: {url}...")
root.after(0, lambda: output_text_widget.delete('1.0', tk.END))
root.after(0, lambda: output_text_widget.insert(tk.END, f"--- EXTRACCIÓN EN CURSO: {url} ---\n\n"))
try:
# 3. Realizar la solicitud HTTP con headers mejorados
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Referer': 'https://www.google.com/',
'Connection': 'keep-alive',
}
response = requests.get(url, headers=headers, timeout=30) # Aumento timeout por seguridad
response.raise_for_status()
# 4. Analizar el contenido
soup = BeautifulSoup(response.text, 'html.parser')
result_text = ""
# 5. Extracción basada en el tipo seleccionado
# --- EXTRACCIÓN COMBINADA AMAZON (SIN LÍMITE) ---
if tipo_extraccion == "Portátiles Gamer (Enlace + Precio)":
# Selector de contenedor genérico para cada resultado de Amazon
PRODUCT_CONTAINER = "div.s-result-item"
containers = soup.select(PRODUCT_CONTAINER)
if containers:
result_text += f"--- {len(containers)} CONTENEDORES DE PRODUCTO ENCONTRADOS --- \n\n"
for i, container in enumerate(containers):
# 1. Encontrar el enlace/título principal: Usamos el selector que funciona para el enlace.
link_tag = container.select_one('h2 a')
if not link_tag:
# Selector de respaldo que te funcionó parcialmente antes
link_tag = container.select_one('a.a-link-normal.s-underline-text.s-underline-link-text.s-link-style.a-text-normal')
# 2. Extraer el título: Buscamos el SPAN que contiene el texto del título (clase usada por Amazon)
title_span = container.select_one('span.a-text-normal')
# 3. Extraer Precio (dentro del contenedor)
price_whole_tag = container.select_one('span.a-price-whole')
price_symbol_tag = container.select_one('span.a-price-symbol')
title = title_span.get_text(strip=True) if title_span else "N/A (Título Span Falló)"
link = link_tag.get('href') if link_tag else "N/A"
price = f"{price_whole_tag.get_text(strip=True)}{price_symbol_tag.get_text(strip=True)}" if price_whole_tag and price_symbol_tag else "Precio No Encontrado"
# Formato de Salida
result_text += f"[{i+1}] TÍTULO: {title}\n"
result_text += f" PRECIO: {price}\n"
# Manejo de enlaces relativos de Amazon
if link.startswith('/'):
result_text += f" ENLACE: https://www.amazon.es{link}\n"
else:
result_text += f" ENLACE: {link}\n"
result_text += "---------------------------------------\n"
else:
result_text += f"ERROR: No se encontraron contenedores de producto con el selector: '{PRODUCT_CONTAINER}'.\n"
# --- MODOS BÁSICOS Y AVANZADOS (SIN LÍMITE) ---
elif tipo_extraccion == "Título y Metadatos":
title = soup.title.string if soup.title else "N/A"
description_tag = soup.find('meta', attrs={'name': 'description'})
desc_content = description_tag.get('content') if description_tag else "N/A"
result_text += f"TÍTULO: {title}\n"
result_text += f"DESCRIPCIÓN: {desc_content}\n"
elif tipo_extraccion == "Primeros Párrafos":
paragraphs = soup.find_all('p', limit=10)
if paragraphs:
for i, p in enumerate(paragraphs):
text = p.get_text(strip=True)
result_text += f"PARRAFO {i+1}:\n{text[:300]}{'...' if len(text) > 300 else ''}\n\n"
else:
result_text += "No se encontraron párrafos.\n"
elif tipo_extraccion == "Enlaces (Links)":
links = soup.find_all('a', href=True)
if links:
for i, link in enumerate(links):
text = link.get_text(strip=True)[:50] or "Link sin texto"
result_text += f"[{i+1}] TEXTO: {text} \n URL: {link['href']}\n\n"
else:
result_text += "No se encontraron enlaces.\n"
elif tipo_extraccion == "Imágenes (URLs)":
images = soup.find_all('img', src=True)
if images:
for i, img in enumerate(images):
alt_text = img.get('alt', 'N/A')
result_text += f"[{i+1}] ALT: {alt_text[:50]} \n URL: {img['src']}\n\n"
else:
result_text += "No se encontraron etiquetas de imagen ().\n"
elif tipo_extraccion == "Tablas (Estructura Básica)":
tables = soup.find_all('table', limit=5)
if tables:
for i, table in enumerate(tables):
result_text += f"\n--- TABLA {i+1} ---\n"
rows = table.find_all(['tr'])
for row in rows[:10]:
cols = row.find_all(['td', 'th'])
row_data = [re.sub(r'\s+', ' ', col.get_text(strip=True)) for col in cols]
result_text += " | ".join(row_data) + "\n"
result_text += "--- FIN TABLA ---\n"
else:
result_text += "No se encontraron tablas (