From 247021823c279be14bd896b70c773ac4e6e28784 Mon Sep 17 00:00:00 2001 From: mireya Date: Sun, 7 Dec 2025 19:58:07 +0000 Subject: [PATCH] Subir archivos a "/" --- main.py | 688 +++++++++++++++++++++++++++++++++++++++++++++ monitor_logic.py | 117 ++++++++ network_monitor.py | 51 ++++ notepad_logic.py | 39 +++ scraping_logic.py | 147 ++++++++++ 5 files changed, 1042 insertions(+) create mode 100644 main.py create mode 100644 monitor_logic.py create mode 100644 network_monitor.py create mode 100644 notepad_logic.py create mode 100644 scraping_logic.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..c22263e --- /dev/null +++ b/main.py @@ -0,0 +1,688 @@ +import tkinter as tk +from tkinter import Menu, ttk, messagebox, filedialog +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import os +import threading # Necesario para lanzar el scraper en un hilo + +# Importaciones de módulos de lógica +import utility_thread as ut +import monitor_logic as ml +import alarm_logic as al +import backup_logic as bl +import notepad_logic as nl +import network_monitor as nm +import camel_game_logic as cgl +import audio_player_logic as apl +import external_launcher as el +import scraping_logic as sl # as 'sl' apunta al scraper de Amazon + +# --- CLASE ALARMLISTFRAME --- +class AlarmListFrame(tk.Frame): + def __init__(self, master, root_app, **kwargs): + super().__init__(master, **kwargs) + self.root_app = root_app + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + self.tree = ttk.Treeview(self, columns=('Time', 'Message', 'Status'), show='headings') + self.tree.heading('Time', text='Hora') + self.tree.heading('Message', text='Mensaje') + self.tree.heading('Status', text='Estado') + self.tree.column('Time', width=80, anchor=tk.CENTER) + self.tree.column('Status', width=80, anchor=tk.CENTER) + + self.tree.grid(row=0, column=0, sticky='nsew', padx=5, pady=5) + + vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) + vsb.grid(row=0, column=1, sticky='ns') + self.tree.configure(yscrollcommand=vsb.set) + + self.tree.bind('<>', self.on_select) + + self.update_list() + + def update_list(self, alarms=None): + if alarms is None: + alarms = al.load_alarms() + + for item in self.tree.get_children(): + self.tree.delete(item) + + for alarm in alarms: + time_str = f"{alarm['hour']:02d}:{alarm['minute']:02d}" + status_str = "ACTIVA" if alarm['active'] else "INACTIVA" + tag = 'active' if alarm['active'] else 'inactive' + + self.tree.insert('', tk.END, iid=str(alarm['id']), text='', + values=(time_str, alarm['message'], status_str), + tags=(tag,)) + + self.tree.tag_configure('active', background='#D4EDDA', foreground='#155724') + self.tree.tag_configure('inactive', background='#F8D7DA', foreground='#721C24') + + def on_select(self, event): + selected_items = self.tree.selection() + if not selected_items: + self.root_app.tree_deselect_and_disable_buttons() + return + + alarm_id = int(selected_items[0]) + alarms = al.load_alarms() + + selected_alarm = next((a for a in alarms if a['id'] == alarm_id), None) + + if selected_alarm: + self.root_app.set_selected_alarm(alarm_id, selected_alarm['active']) + + +# --- CLASE MAINAPPLICATION --- +class MainApplication(tk.Tk): + def __init__(self): + super().__init__() + self.title("Ventana Responsive Modular") + self.geometry("1000x700") + + self.root = self + self.monitor_elements = ml.initialize_monitor_figures() + self.selected_alarm_id = None + self.selected_alarm_is_active = False + self.camel_threads = [] + self.winner_name = None + # Variables para el scraping + self.scraping_search_term_var = None + self.scraping_results_text = None + self.DEBUG_HTML_FILE = "amazon_debugging_output.html" + + + self.configure_layout() + self.create_widgets() + + ut.start_time_thread(self.label_fecha_hora) + self.start_monitor() + self.start_alarm_checker() + nm.start_network_monitoring_thread(self) + + # INICIALIZAR EL REPRODUCTOR DE MÚSICA + self.music_player = apl.MusicPlayer(self) + self.protocol("WM_DELETE_WINDOW", self.on_closing) + + def on_closing(self): + """Detiene la música y destruye la ventana al cerrar la app.""" + self.music_player.stop_music() + self.destroy() + + def configure_layout(self): + self.columnconfigure(0, weight=0) + self.columnconfigure(1, weight=1) + self.columnconfigure(2, weight=0) + self.rowconfigure(0, weight=1) + self.rowconfigure(1, weight=0) + + def create_widgets(self): + self.create_menu() + self.create_frames() + self.create_left_panel_controls() + self.create_central_subframes() + self.create_status_bar() + self.create_notebook() + + self.create_monitor_tab() + self.create_alarm_tab() + self.create_notepad_tab() + self.create_camel_game_tab() + self.create_audio_player_tab() + self.create_external_launcher_tab() + self.create_scraping_tab() + + self.notebook.bind('<>', self.on_tab_change) + self.update_activity_status("Inicio de la aplicación") + + + def create_menu(self): + menu_bar = Menu(self) + file_menu = Menu(menu_bar, tearoff=0) + file_menu.add_command(label="Salir", command=self.on_closing) + menu_bar.add_cascade(label="Archivo", menu=file_menu) + self.config(menu=menu_bar) + + def create_frames(self): + self.frame_izquierdo = tk.Frame(self, bg="lightblue", width=200) + self.frame_central = tk.Frame(self, bg="white") + self.frame_derecho = tk.Frame(self, bg="lightgreen", width=200) + + self.frame_izquierdo.grid(row=0, column=0, sticky="ns") + self.frame_central.grid(row=0, column=1, sticky="nsew") + self.frame_derecho.grid(row=0, column=2, sticky="ns") + + self.frame_izquierdo.grid_propagate(False) + self.frame_derecho.grid_propagate(False) + + def create_left_panel_controls(self): + self.frame_izquierdo.columnconfigure(0, weight=1) + + tk.Label(self.frame_izquierdo, text="Procesos batch", font=("Arial", 12, "bold"), bg="lightblue").grid(row=0, column=0, padx=10, pady=(10, 5), sticky="w") + + btn_backup = tk.Button( + self.frame_izquierdo, + text="Copias de seguridad", + command=self.backup_app_state, + bg="#D4EDDA", + fg="#155724", + font=("Arial", 10, "bold"), + relief=tk.RAISED, + bd=3 + ) + btn_backup.grid(row=1, column=0, padx=10, pady=5, sticky="ew") + + def create_central_subframes(self): + self.frame_central.rowconfigure(0, weight=1) + self.frame_central.rowconfigure(1, weight=0) + self.frame_central.columnconfigure(0, weight=1) + + self.frame_superior = tk.Frame(self.frame_central, bg="lightyellow") + self.frame_inferior = tk.Frame(self.frame_central, bg="lightgray", height=100) + + self.frame_superior.grid(row=0, column=0, sticky="nsew") + self.frame_inferior.grid(row=1, column=0, sticky="ew") + self.frame_inferior.grid_propagate(False) + + def create_status_bar(self): + self.barra_estado = tk.Label(self, bg="lightgray", anchor="w") + self.barra_estado.grid(row=1, column=0, columnspan=3, sticky="ew") + + self.label_status_1 = tk.Label(self.barra_estado, text="Listo", bg="green", anchor="w", fg='white') + self.label_status_1.pack(side="left", fill="x", expand=True) + + self.label_net_status = tk.Label(self.barra_estado, text="Red: 0.00/0.00 KB", bg="blue", anchor="w", fg='white') + self.label_net_status.pack(side="left", fill="x", expand=True) + + self.label_audio_status = tk.Label(self.barra_estado, text="Música: Ninguno", bg="cyan", anchor="w") + self.label_audio_status.pack(side="left", fill="x", expand=True) + + self.label_fecha_hora = tk.Label(self.barra_estado, text="Hilo fecha-hora", font=("Helvetica", 14), bd=1, fg="blue", relief="sunken", anchor="w", width=20, padx=10) + self.label_fecha_hora.pack(side="right", fill="x", expand=True) + + def create_notebook(self): + style = ttk.Style() + style.configure("CustomNotebook.TNotebook.Tab", font=("Arial", 12, "bold")) + self.notebook = ttk.Notebook(self.frame_superior, style="CustomNotebook.TNotebook") + self.notebook.pack(fill="both", expand=True) + + + + def create_monitor_tab(self): + tab_monitor = ttk.Frame(self.notebook) + self.notebook.add(tab_monitor, text="Monitor de Sistema 📈", padding=4) + + tab_monitor.columnconfigure(0, weight=1); tab_monitor.columnconfigure(1, weight=1) + tab_monitor.rowconfigure(0, weight=1); tab_monitor.rowconfigure(1, weight=0) + + self.canvas_usage = FigureCanvasTkAgg(self.monitor_elements['fig_usage'], master=tab_monitor) + self.canvas_widget_usage = self.canvas_usage.get_tk_widget() + self.canvas_widget_usage.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + + self.canvas_net = FigureCanvasTkAgg(self.monitor_elements['fig_net'], master=tab_monitor) + self.canvas_widget_net = self.canvas_net.get_tk_widget() + self.canvas_widget_net.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) + + frame_metrics = ttk.Frame(tab_monitor, padding=10, relief="groove") + frame_metrics.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5) + frame_metrics.columnconfigure(0, weight=1); frame_metrics.columnconfigure(1, weight=1) + + self.label_load = tk.Label(frame_metrics, text="Carga: -", anchor="w", font=("Arial", 10, "bold")) + self.label_load.grid(row=0, column=0, sticky="ew", pady=2) + + self.label_threads = tk.Label(frame_metrics, text="Hilos: -", anchor="w", font=("Arial", 10, "bold")) + self.label_threads.grid(row=0, column=1, sticky="ew", pady=2) + + def create_alarm_tab(self): + tab_alarm = ttk.Frame(self.notebook) + self.notebook.add(tab_alarm, text="Gestor de Alarmas ⏰", padding=10) + + tab_alarm.columnconfigure(0, weight=1) + tab_alarm.columnconfigure(1, weight=0) + tab_alarm.rowconfigure(0, weight=1) + + frame_lista = ttk.Frame(tab_alarm) + frame_lista.grid(row=0, column=0, sticky='nsew', padx=10, pady=10) + frame_lista.columnconfigure(0, weight=1) + frame_lista.rowconfigure(0, weight=1) + frame_lista.rowconfigure(1, weight=0) + + self.alarm_list_widget = AlarmListFrame(frame_lista, self, relief='sunken', borderwidth=2) + self.alarm_list_widget.grid(row=0, column=0, sticky='nsew') + + self.alarm_notification_label = tk.Label(frame_lista, text="Sistema de Alarmas Activo", fg='green', font=("Arial", 14, "bold")) + self.alarm_notification_label.grid(row=1, column=0, sticky='ew', pady=5) + + frame_control = ttk.Frame(tab_alarm, padding=15, relief='groove') + frame_control.grid(row=0, column=1, sticky='ns', padx=10, pady=10) + + ttk.Label(frame_control, text="Añadir Nueva Alarma", font=("Arial", 12, "bold")).grid(row=0, column=0, columnspan=2, pady=(0, 10)) + + ttk.Label(frame_control, text="Hora (HH):").grid(row=1, column=0, sticky='w', pady=5) + self.alarm_hour_var = tk.StringVar(value="08") + ttk.Entry(frame_control, textvariable=self.alarm_hour_var, width=5).grid(row=1, column=1, sticky='e', pady=5) + + ttk.Label(frame_control, text="Minuto (MM):").grid(row=2, column=0, sticky='w', pady=5) + self.alarm_minute_var = tk.StringVar(value="00") + ttk.Entry(frame_control, textvariable=self.alarm_minute_var, width=5).grid(row=2, column=1, sticky='e', pady=5) + + ttk.Label(frame_control, text="Mensaje:").grid(row=3, column=0, sticky='w', pady=5) + self.alarm_message_var = tk.StringVar(value="Recordatorio") + ttk.Entry(frame_control, textvariable=self.alarm_message_var, width=15).grid(row=3, column=1, sticky='e', pady=5) + + ttk.Button(frame_control, text="Programar Alarma", command=self.add_alarm_callback).grid(row=4, column=0, columnspan=2, pady=(10, 20), sticky='ew') + + ttk.Label(frame_control, text="Control de Alarma Seleccionada", font=("Arial", 12, "bold")).grid(row=5, column=0, columnspan=2, pady=(10, 10)) + + self.btn_toggle = ttk.Button(frame_control, text="Activar / Desactivar", command=self.toggle_alarm_callback, state=tk.DISABLED) + self.btn_toggle.grid(row=6, column=0, columnspan=2, pady=5, sticky='ew') + + self.btn_delete = ttk.Button(frame_control, text="Eliminar Alarma", command=self.delete_alarm_callback, state=tk.DISABLED) + self.btn_delete.grid(row=7, column=0, columnspan=2, pady=5, sticky='ew') + + def create_notepad_tab(self): + tab_notepad = ttk.Frame(self.notebook) + self.notebook.add(tab_notepad, text="Notas 📝", padding=10) + + tab_notepad.columnconfigure(0, weight=1) + tab_notepad.rowconfigure(0, weight=1) + tab_notepad.rowconfigure(1, weight=0) + + frame_editor = ttk.Frame(tab_notepad) + frame_editor.grid(row=0, column=0, sticky='nsew', padx=5, pady=5) + frame_editor.columnconfigure(0, weight=1) + frame_editor.rowconfigure(0, weight=1) + + self.text_editor = tk.Text( + frame_editor, + wrap=tk.WORD, + font=("Consolas", 10), + padx=10, + pady=10 + ) + self.text_editor.grid(row=0, column=0, sticky='nsew') + + vsb = ttk.Scrollbar(frame_editor, orient="vertical", command=self.text_editor.yview) + vsb.grid(row=0, column=1, sticky='ns') + self.text_editor.configure(yscrollcommand=vsb.set) + + self.load_notes_callback(startup=True) + + frame_buttons = ttk.Frame(tab_notepad) + frame_buttons.grid(row=1, column=0, sticky='ew', padx=5, pady=(0, 5)) + frame_buttons.columnconfigure(0, weight=1) + frame_buttons.columnconfigure(1, weight=1) + + ttk.Button(frame_buttons, text="Guardar Notas 💾", command=self.save_notes_callback).grid(row=0, column=0, sticky='ew', padx=5, pady=5) + + ttk.Button(frame_buttons, text="Recargar (Deshacer) 🔄", command=self.load_notes_callback).grid(row=0, column=1, sticky='ew', padx=5, pady=5) + + def create_camel_game_tab(self): + tab_game = ttk.Frame(self.notebook) + self.notebook.add(tab_game, text="Carrera de Camellos 🏁", padding=10) + + tab_game.columnconfigure(0, weight=1) + tab_game.rowconfigure(0, weight=0) + tab_game.rowconfigure(1, weight=1) + + frame_control = ttk.Frame(tab_game, padding=5) + frame_control.grid(row=0, column=0, sticky='ew') + + ttk.Style().configure("Accent.TButton", font=("Arial", 12, "bold"), foreground="blue") + ttk.Button(frame_control, text="¡INICIAR CARRERA!", command=self.start_game_callback, style="Accent.TButton").pack(pady=10) + + self.frame_game = ttk.Frame(tab_game, padding=10, relief='groove', borderwidth=2) + self.frame_game.grid(row=1, column=0, sticky='nsew', padx=5, pady=5) + self.frame_game.columnconfigure(0, weight=1) + + tk.Label(self.frame_game, text="Pulsa 'INICIAR CARRERA' para empezar...", fg='gray', font=("Arial", 14)).pack(pady=20) + + def create_audio_player_tab(self): + tab_audio = ttk.Frame(self.notebook) + self.notebook.add(tab_audio, text="Música 🎧", padding=20) + + ttk.Label(tab_audio, text="Reproductor de Música de Fondo (Pygame)", font=("Arial", 16, "bold")).pack(pady=10) + + self.audio_filepath_var = tk.StringVar(value="Ningún archivo cargado.") + + ttk.Label(tab_audio, textvariable=self.audio_filepath_var, wraplength=500).pack(pady=(10, 5)) + + ttk.Button(tab_audio, text="Seleccionar Archivo MP3/OGG", command=self.select_audio_file).pack(pady=10) + + frame_controls = ttk.Frame(tab_audio) + frame_controls.pack(pady=20) + + ttk.Button(frame_controls, text="⏸️ Pausa", command=self.pause_audio).pack(side=tk.LEFT, padx=10) + ttk.Button(frame_controls, text="▶️ Reanudar", command=self.unpause_audio).pack(side=tk.LEFT, padx=10) + ttk.Button(frame_controls, text="⏹️ Detener", command=self.stop_audio).pack(side=tk.LEFT, padx=10) + + def create_external_launcher_tab(self): + tab_external = ttk.Frame(self.notebook) + self.notebook.add(tab_external, text="Lanzador 🌐", padding=20) + + tab_external.columnconfigure(0, weight=1) + tab_external.rowconfigure(0, weight=0) + + ttk.Label(tab_external, text="Lanzar Aplicaciones Externas", font=("Arial", 16, "bold")).pack(pady=10) + + frame_browser = ttk.LabelFrame(tab_external, text="Abrir Navegador", padding=15) + frame_browser.pack(fill='x', pady=10, padx=50) + + ttk.Label(frame_browser, text="URL (ej: google.com):").pack(pady=5) + self.url_var = tk.StringVar(value="www.google.com") + ttk.Entry(frame_browser, textvariable=self.url_var, width=50).pack(pady=5) + + ttk.Button(frame_browser, text="Abrir URL en Navegador Predeterminado", command=self.launch_browser_callback).pack(pady=10) + + frame_custom = ttk.LabelFrame(tab_external, text="Abrir Aplicación (Ej: Bloc de Notas)", padding=15) + frame_custom.pack(fill='x', pady=20, padx=50) + + ttk.Label(frame_custom, text="Comando (ej: notepad.exe):").pack(pady=5) + self.app_command_var = tk.StringVar(value="notepad.exe") + ttk.Entry(frame_custom, textvariable=self.app_command_var, width=50).pack(pady=5) + + ttk.Button(frame_custom, text="Lanzar Aplicación Externa", command=self.launch_custom_app_callback).pack(pady=10) + + # --- MÉTODO PARA LA SOLAPA WEB SCRAPING --- + def create_scraping_tab(self): + tab_scraping = ttk.Frame(self.notebook) + self.notebook.add(tab_scraping, text="Web Scraper 🔎", padding=15) + + tab_scraping.columnconfigure(0, weight=1) + tab_scraping.rowconfigure(1, weight=1) + + # 1. Controles Superiores (Término y Botón) + frame_controls = ttk.Frame(tab_scraping, padding=10) + frame_controls.grid(row=0, column=0, sticky='ew', pady=(0, 10)) + frame_controls.columnconfigure(1, weight=1) + + ttk.Label(frame_controls, text="Término de Búsqueda (Ej: Portátil, Auriculares):").grid(row=0, column=0, padx=5, sticky='w') + + self.scraping_search_term_var = tk.StringVar(value="auriculares bluetooth") + ttk.Entry(frame_controls, textvariable=self.scraping_search_term_var, width=40).grid(row=0, column=1, padx=5, sticky='ew') + + ttk.Button(frame_controls, text="START SCRAPING", command=self.start_scraping_callback).grid(row=0, column=2, padx=10, sticky='e') + + # 2. Área de Resultados (Texto) + frame_results = ttk.Frame(tab_scraping, relief=tk.SUNKEN, borderwidth=2) + frame_results.grid(row=1, column=0, sticky='nsew', padx=5, pady=5) + frame_results.columnconfigure(0, weight=1) + frame_results.rowconfigure(0, weight=1) + + self.scraping_results_text = tk.Text( + frame_results, + wrap=tk.WORD, + font=("Consolas", 10), + padx=10, + pady=10 + ) + self.scraping_results_text.grid(row=0, column=0, sticky='nsew') + + vsb = ttk.Scrollbar(frame_results, orient="vertical", command=self.scraping_results_text.yview) + vsb.grid(row=0, column=1, sticky='ns') + self.scraping_results_text.configure(yscrollcommand=vsb.set) + + # --- MÉTODOS DE CALLBACKS DE LÓGICA --- + + # ------------------------------------------------------------------ + # GESTIÓN DEL SCRAPING DE AMAZON (Llamada al hilo) + # ------------------------------------------------------------------ + def start_scraping_callback(self): + """Inicia el proceso de scraping en un hilo separado (llama a Playwright).""" + search_term = self.scraping_search_term_var.get() + if not search_term: + messagebox.showwarning("Advertencia", "Por favor, introduce un término de búsqueda.") + return + + self.update_activity_status(f"Iniciando scraping (Playwright) para: {search_term}...") + self.scraping_results_text.delete("1.0", tk.END) + self.scraping_results_text.insert("1.0", f"Buscando productos en Amazon para '{search_term}' usando Playwright. Esto puede tardar unos segundos...\n") + + # LLAMADA CLAVE: Inicia el proceso asíncrono en un hilo de trabajo + sl.start_playwright_scraper(search_term, self) + + def _display_scraping_results(self, results, search_term): + """ + Recibe los resultados del hilo de Playwright (llamado vía self.after) + y los muestra en la interfaz. + """ + self.scraping_results_text.delete("1.0", tk.END) + + if results and "error" in results[0]: + output = f"--- ERROR en Scraping para {search_term} ---\n\n{results[0]['error']}\n\nSi obtienes un Timeout (30s), Amazon te ha bloqueado." + self.scraping_results_text.insert("1.0", output) + self.update_activity_status(f"Scraping fallido para {search_term}.") + return + + output = f"--- PRODUCTOS de Amazon para '{search_term}' ({len(results)} encontrados) ---\n\n" + + for i, product in enumerate(results): + output += f"[{i+1}] Producto: {product['nombre']}\n" + output += f" Precio: {product['precio']}\n" + output += f" Vendedor/Marca: {product['vendedor']}\n" + output += f" URL Imagen: {product['imagen_url']}\n" + output += "-" * 50 + "\n" + + output += f"\nDatos guardados en {sl.SCRAPING_FILE}." + + self.scraping_results_text.insert("1.0", output) + self.update_activity_status(f"Scraping completado para '{search_term}'. Resultados guardados.") + # ------------------------------------------------------------------ + + + # --- GESTIÓN DE AUDIO --- + + def select_audio_file(self): + filepath = filedialog.askopenfilename( + defaultextension=".mp3", + filetypes=[("Archivos de Audio", "*.mp3 *.ogg"), ("Todos los archivos", "*.*")] + ) + if filepath: + self.audio_filepath_var.set(filepath) + self.update_activity_status(f"Archivo seleccionado: {os.path.basename(filepath)}") + self.music_player.load_and_play(filepath) + + def pause_audio(self): + self.music_player.pause_music() + self.update_activity_status("Música pausada.") + + def unpause_audio(self): + self.music_player.unpause_music() + self.update_activity_status("Música reanudada.") + + def stop_audio(self): + self.music_player.stop_music() + self.audio_filepath_var.set("Ningún archivo cargado.") + self.update_activity_status("Música detenida.") + + def update_audio_status(self, message): + self.label_audio_status.config(text=f"{message}") + + # --- GESTIÓN DE LANZADOR EXTERNO --- + + def launch_browser_callback(self): + url = self.url_var.get() + if el.launch_browser(url): + self.update_activity_status(f"Lanzando URL: {url}") + else: + self.update_activity_status("Error al lanzar el navegador.") + + def launch_custom_app_callback(self): + command = self.app_command_var.get().split() + + if not command: + messagebox.showwarning("Advertencia", "El campo de comando no puede estar vacío.") + return + + app_name = command[0] + if el.launch_custom_app(command, app_name): + self.update_activity_status(f"Lanzando aplicación: {app_name}") + else: + self.update_activity_status(f"Fallo al lanzar la aplicación: {app_name}") + + + # --- GESTIÓN DE ESTADO Y UTILIDADES --- + + def on_tab_change(self, event): + selected_tab = self.notebook.tab(self.notebook.select(), "text") + self.update_activity_status(f"{selected_tab}") + + def update_activity_status(self, message): + self.label_status_1.config(text=f"{message}") + + def update_net_status(self, kb_sent, kb_recv): + text = f"Red: ↑{kb_sent:.2f} / ↓{kb_recv:.2f} KB" + self.label_net_status.config(text=text) + + def start_game_callback(self): + for thread in self.camel_threads: + thread.is_running = False + + self.update_activity_status("Preparando la carrera...") + cgl.start_camel_game(self, track_length=50) + + def clear_camel_game_area(self): + for widget in self.frame_game.winfo_children(): + widget.destroy() + + def show_winner(self, winner_name): + messagebox.showinfo("¡Carrera Terminada!", f"¡HA GANADO {winner_name.upper()}!") + self.update_activity_status(f"¡{winner_name} ha ganado la carrera!") + + def backup_app_state(self): + current_alarms = al.load_alarms() + bl.create_backup(current_alarms) + self.update_activity_status("Copia de seguridad de Alarmas creada.") + + def save_notes_callback(self): + content = self.text_editor.get("1.0", tk.END) + if nl.save_notes(content): + self.text_editor.edit_modified(False) + self.update_activity_status("Notas guardadas correctamente.") + + def load_notes_callback(self, startup=False): + + if not startup and self.text_editor.edit_modified(): + response = messagebox.askyesno( + "Confirmar Recarga", + "El editor tiene cambios sin guardar. ¿Estás seguro de que quieres recargar el archivo (y perder los cambios)?" + ) + if not response: + return + + loaded_content = nl.load_notes() + + self.text_editor.delete("1.0", tk.END) + self.text_editor.insert("1.0", loaded_content) + self.text_editor.edit_modified(False) + if not startup: + self.update_activity_status("Notas recargadas desde el archivo.") + + def set_selected_alarm(self, alarm_id, is_active): + self.selected_alarm_id = alarm_id + self.selected_alarm_is_active = is_active + + self.btn_toggle.config(state=tk.NORMAL) + self.btn_delete.config(state=tk.NORMAL) + + toggle_text = "Desactivar" if is_active else "Activar" + self.btn_toggle.config(text=toggle_text) + self.update_activity_status(f"Alarma {alarm_id} seleccionada (Estado: {'ACTIVA' if is_active else 'INACTIVA'}).") + + + def add_alarm_callback(self): + try: + hour = self.alarm_hour_var.get() + minute = self.alarm_minute_var.get() + message = self.alarm_message_var.get() + + if not (0 <= int(hour) <= 23 and 0 <= int(minute) <= 59): + messagebox.showerror("Error", "La hora debe estar entre 00 y 23, y el minuto entre 00 y 59.") + return + + alarms = al.add_alarm(hour, minute, message) + self.alarm_list_widget.update_list(alarms) + self.alarm_notification_label.config(text="Alarma programada con éxito.", fg='blue') + self.update_activity_status(f"Alarma programada: {hour}:{minute}.") + + + except ValueError: + messagebox.showerror("Error", "Por favor, introduce números válidos para hora y minuto.") + + def toggle_alarm_callback(self): + if self.selected_alarm_id is not None: + new_state = not self.selected_alarm_is_active + alarms = al.toggle_alarm(self.selected_alarm_id, new_state) + self.alarm_list_widget.update_list(alarms) + self.alarm_notification_label.config(text=f"Alarma {self.selected_alarm_id} {'ACTIVADA' if new_state else 'DESACTIVADA'}.", fg='green' if new_state else 'orange') + self.tree_deselect_and_disable_buttons() + self.update_activity_status(f"Alarma {self.selected_alarm_id} {'ACTIVADA' if new_state else 'DESACTIVADA'}.") + + + def delete_alarm_callback(self): + if self.selected_alarm_id is not None: + alarms = al.delete_alarm(self.selected_alarm_id) + self.alarm_list_widget.update_list(alarms) + self.alarm_notification_label.config(text=f"Alarma {self.selected_alarm_id} eliminada.", fg='red') + self.tree_deselect_and_disable_buttons() + self.update_activity_status(f"Alarma {self.selected_alarm_id} eliminada.") + + + def tree_deselect_and_disable_buttons(self): + if self.alarm_list_widget.tree.selection(): + self.alarm_list_widget.tree.selection_remove(self.alarm_list_widget.tree.selection()) + self.selected_alarm_id = None + self.btn_toggle.config(state=tk.DISABLED) + self.btn_delete.config(state=tk.DISABLED) + + def show_alarm_popup(self, alarm_data): + + self.alarm_notification_label.config(text=f"¡ALARMA SONANDO!", fg='red') + + dialog = al.CustomAlarmDialog(self, alarm_data) + result = dialog.result + + if result == 'posponer': + alarms = al.postpone_alarm(alarm_data['id']) + self.alarm_notification_label.config(text=f"Alarma pospuesta 1 minuto.", fg='blue') + self.update_activity_status(f"Alarma {alarm_data['id']} pospuesta.") + + + elif result == 'detener': + alarms = al.toggle_alarm(alarm_data['id'], False) + self.alarm_notification_label.config(text=f"Alarma detenida.", fg='green') + self.update_activity_status(f"Alarma {alarm_data['id']} detenida.") + + + else: + alarms = al.toggle_alarm(alarm_data['id'], False) + self.alarm_notification_label.config(text=f"Alarma detenida (cierre inesperado).", fg='green') + self.update_activity_status(f"Alarma {alarm_data['id']} detenida (cierre inesperado).") + + + self.alarm_list_widget.update_list() + self.tree_deselect_and_disable_buttons() + + + def start_monitor(self): + self.after( + 100, + ml.update_monitor, + self.root, + self.monitor_elements['lines'], + self.monitor_elements['ax_net'], + self.canvas_usage, + self.canvas_net, + self.label_load, + self.label_threads + ) + + def start_alarm_checker(self): + self.after(100, al.check_alarms, self.root, self.alarm_list_widget, self.show_alarm_popup) + + +if __name__ == "__main__": + app = MainApplication() + app.mainloop() \ No newline at end of file diff --git a/monitor_logic.py b/monitor_logic.py new file mode 100644 index 0000000..454554d --- /dev/null +++ b/monitor_logic.py @@ -0,0 +1,117 @@ +# monitor_logic.py +import psutil +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from matplotlib.figure import Figure +import os + +# --- Variables Globales y Estado Inicial --- +MAX_SAMPLES = 60 +historical_data = { + 'time': list(range(1, MAX_SAMPLES + 1)), + 'cpu': [0] * MAX_SAMPLES, + 'memory': [0] * MAX_SAMPLES, + 'disk': [0] * MAX_SAMPLES, + 'net_sent': [0] * MAX_SAMPLES, + 'net_recv': [0] * MAX_SAMPLES +} + +LAST_NET_COUNTERS = psutil.net_io_counters() +CURRENT_PROCESS = psutil.Process(os.getpid()) + + +def initialize_monitor_figures(): + """ + Define y devuelve los objetos de figura, ejes y líneas de Matplotlib. + Esto permite inicializar el monitor una sola vez al inicio. + """ + + # --- Gráfico de Uso del Sistema (CPU, Memoria, Disco) --- + fig_usage = Figure(figsize=(4, 6), dpi=100) + fig_usage.subplots_adjust(hspace=0.5) + + ax1 = fig_usage.add_subplot(311) + line_cpu, = ax1.plot(historical_data['time'], historical_data['cpu'], 'r-') + ax1.set_title("Uso de CPU (%)", fontsize=10); ax1.set_ylabel("CPU (%)", fontsize=8); ax1.set_ylim(0, 100); ax1.set_xlim(1, MAX_SAMPLES); ax1.grid(True); ax1.tick_params(axis='x', labelbottom=False) + + ax2 = fig_usage.add_subplot(312, sharex=ax1) + line_mem, = ax2.plot(historical_data['time'], historical_data['memory'], 'b-') + ax2.set_title("Uso de Memoria (%)", fontsize=10); ax2.set_ylabel("Memoria (%)", fontsize=8); ax2.set_ylim(0, 100); ax2.set_xlim(1, MAX_SAMPLES); ax2.grid(True); ax2.tick_params(axis='x', labelbottom=False) + + ax3 = fig_usage.add_subplot(313, sharex=ax1) + line_disk, = ax3.plot(historical_data['time'], historical_data['disk'], 'g-') + ax3.set_title("Uso de Disco (%)", fontsize=10); ax3.set_xlabel("Tiempo (segundos)", fontsize=8); ax3.set_ylabel("Disco (%)", fontsize=8); ax3.set_ylim(0, 100); ax3.set_xlim(1, MAX_SAMPLES); ax3.grid(True) + fig_usage.tight_layout(pad=1.5) + + # --- Gráfico de Red (Bytes Enviados/Recibidos) --- + fig_net = Figure(figsize=(4, 6), dpi=100) + fig_net.subplots_adjust(hspace=0.5) + ax_net = fig_net.add_subplot(111) + + line_net_sent, = ax_net.plot(historical_data['time'], historical_data['net_sent'], 'm-', label='Enviado') + line_net_recv, = ax_net.plot(historical_data['time'], historical_data['net_recv'], 'c-', label='Recibido') + ax_net.set_title("Tráfico de Red (Bytes/s)", fontsize=10); ax_net.set_xlabel("Tiempo (segundos)", fontsize=8); ax_net.set_ylabel("Bytes/s", fontsize=8); ax_net.set_ylim(0, 500000); ax_net.set_xlim(1, MAX_SAMPLES); ax_net.legend(loc='upper left', fontsize=8); ax_net.grid(True) + fig_net.tight_layout(pad=1.5) + + # Devuelve todos los elementos que se necesitan para la actualización + return { + 'fig_usage': fig_usage, + 'fig_net': fig_net, + 'lines': { + 'cpu': line_cpu, 'mem': line_mem, 'disk': line_disk, + 'net_sent': line_net_sent, 'net_recv': line_net_recv + }, + 'ax_net': ax_net # Para auto-escalado + } + + +def update_monitor(root, lines, ax_net, mpl_canvas_usage, mpl_canvas_net, label_load, label_threads): + """ + Obtiene los datos de psutil y actualiza todos los gráficos. + Esta función debe ser llamada por root.after() + """ + global LAST_NET_COUNTERS + + # 1. Obtener los nuevos datos del sistema + cpu_percent = psutil.cpu_percent(interval=None) + mem_percent = psutil.virtual_memory().percent + disk_percent = psutil.disk_usage('/').percent + load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else (0.0, 0.0, 0.0) + thread_count = CURRENT_PROCESS.num_threads() + + # 2. Calcular el Tráfico de Red (Bytes/s) + current_net_counters = psutil.net_io_counters() + bytes_sent_rate = (current_net_counters.bytes_sent - LAST_NET_COUNTERS.bytes_sent) / 1.0 + bytes_recv_rate = (current_net_counters.bytes_recv - LAST_NET_COUNTERS.bytes_recv) / 1.0 + LAST_NET_COUNTERS = current_net_counters + + # 3. Actualizar los Labels + label_load.config(text=f"Carga (1m/5m/15m): {load_avg[0]:.2f} / {load_avg[1]:.2f} / {load_avg[2]:.2f}") + label_threads.config(text=f"Hilos del Proceso: {thread_count}") + + # 4. Actualizar datos históricos + historical_data['cpu'].pop(0); historical_data['cpu'].append(cpu_percent) + historical_data['memory'].pop(0); historical_data['memory'].append(mem_percent) + historical_data['disk'].pop(0); historical_data['disk'].append(disk_percent) + historical_data['net_sent'].pop(0); historical_data['net_sent'].append(bytes_sent_rate) + historical_data['net_recv'].pop(0); historical_data['net_recv'].append(bytes_recv_rate) + + # 5. Actualizar las líneas de los gráficos + lines['cpu'].set_ydata(historical_data['cpu']) + lines['mem'].set_ydata(historical_data['memory']) + lines['disk'].set_ydata(historical_data['disk']) + lines['net_sent'].set_ydata(historical_data['net_sent']) + lines['net_recv'].set_ydata(historical_data['net_recv']) + + # Auto-escalado de red + max_net = max(max(historical_data['net_sent']), max(historical_data['net_recv'])) + if max_net * 1.1 > ax_net.get_ylim()[1]: + ax_net.set_ylim(0, max_net * 1.1) + + # 6. Redibujar los canvases + mpl_canvas_usage.draw_idle() + mpl_canvas_net.draw_idle() + + # 7. Llamada recursiva + # Importante: se llama a sí misma en el hilo principal de Tkinter + root.after(1000, update_monitor, root, lines, ax_net, mpl_canvas_usage, mpl_canvas_net, label_load, label_threads) \ No newline at end of file diff --git a/network_monitor.py b/network_monitor.py new file mode 100644 index 0000000..d888ca1 --- /dev/null +++ b/network_monitor.py @@ -0,0 +1,51 @@ +import psutil +import time +import threading + +# Almacenar los contadores iniciales para calcular la diferencia +_initial_counters = None + +def get_net_usage(): + """Obtiene el tráfico de red desde el inicio del proceso y lo devuelve en KB.""" + global _initial_counters + + # 1. Obtener los contadores actuales + net_io = psutil.net_io_counters() + + if _initial_counters is None: + _initial_counters = net_io + # Devuelve 0 para la primera lectura ya que no hay diferencia que calcular + return 0.0, 0.0 + + # 2. Calcular la diferencia (tráfico total desde el inicio de la app) + bytes_sent = net_io.bytes_sent - _initial_counters.bytes_sent + bytes_recv = net_io.bytes_recv - _initial_counters.bytes_recv + + # 3. Convertir a Kilobytes (1024 bytes = 1 KB) + kb_sent = bytes_sent / 1024 + kb_recv = bytes_recv / 1024 + + return kb_sent, kb_recv + +def start_network_monitoring_thread(root_app): + """Inicia un hilo que monitorea el tráfico de red y actualiza la UI.""" + + def monitor_loop(): + # Ejecutar continuamente mientras la aplicación esté viva + while root_app.winfo_exists(): + kb_sent, kb_recv = get_net_usage() + + # Usar after() para actualizar la interfaz de usuario de forma segura + root_app.after( + 0, + root_app.update_net_status, + kb_sent, + kb_recv + ) + + # Esperar 2 segundos antes de la próxima actualización + time.sleep(2) + + # Crear y lanzar el hilo de monitoreo + thread = threading.Thread(target=monitor_loop, daemon=True) + thread.start() \ No newline at end of file diff --git a/notepad_logic.py b/notepad_logic.py new file mode 100644 index 0000000..43b7c49 --- /dev/null +++ b/notepad_logic.py @@ -0,0 +1,39 @@ +import os +from tkinter import messagebox + +# Nombre del archivo donde se guardarán las notas +NOTES_FILE = "notepad_data.txt" + +def save_notes(content): + """ + Guarda el contenido de las notas en un archivo de texto. + :param content: El texto completo del editor. + """ + try: + with open(NOTES_FILE, 'w', encoding='utf-8') as f: + f.write(content) + messagebox.showinfo("Guardado", f"Notas guardadas con éxito en:\n{os.path.abspath(NOTES_FILE)}") + return True + + except Exception as e: + messagebox.showerror("Error de Guardado", f"Error al guardar las notas: {e}") + return False + +def load_notes(): + """ + Carga el contenido de las notas desde el archivo de texto. + :return: El contenido de las notas como string, o un string vacío si no hay archivo. + """ + if not os.path.exists(NOTES_FILE): + return "" + + try: + with open(NOTES_FILE, 'r', encoding='utf-8') as f: + content = f.read() + return content + + except Exception as e: + messagebox.showerror("Error de Carga", f"Error al cargar las notas: {e}") + return "" + +# Fin de notepad_logic.py \ No newline at end of file diff --git a/scraping_logic.py b/scraping_logic.py new file mode 100644 index 0000000..5511f2e --- /dev/null +++ b/scraping_logic.py @@ -0,0 +1,147 @@ +import asyncio +from playwright.async_api import async_playwright +from bs4 import BeautifulSoup +import json +from tkinter import messagebox +import time +import requests +import threading +import os + +# Archivos para persistencia y depuración +SCRAPING_FILE = "amazon_productos_resultados.json" +DEBUG_HTML_FILE = "amazon_debugging_output.html" +# URL BASE de búsqueda de Amazon +BASE_URL = "https://www.amazon.es/s?k={search_term}&ref=nb_sb_noss" + + +def run_scraper_async(search_term, root_app): + """ + Función de punto de entrada para el hilo de Python. + Lanza el bucle asíncrono de Playwright. + """ + try: + asyncio.run(scrape_products_playwright(search_term, root_app)) + except Exception as e: + error_message = f"Error al iniciar el runtime de Playwright: {e}" + root_app.after(0, root_app.update_activity_status, error_message) + root_app.after(0, root_app._display_scraping_results, [{"error": error_message}], search_term) + + +async def scrape_products_playwright(search_term, root_app): + """ + Realiza el scraping usando Playwright con estrategia de espera por tiempo fijo + y extracción basada en la estructura de etiquetas. + """ + search_term_formatted = requests.utils.quote(search_term) + url = BASE_URL.format(search_term=search_term_formatted) + product_data = [] + + try: + async with async_playwright() as p: + # 1. Configuración de Lanzamiento de Navegador (Visible para depuración) + browser = await p.firefox.launch(headless=True, slow_mo=100) + + # 2. Configuración del Contexto de Navegación + context = await browser.new_context( + user_agent='Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0', + locale='es-ES', + viewport={'width': 1920, 'height': 1080}, + accept_downloads=False + ) + page = await context.new_page() + + # Navegar + root_app.after(0, root_app.update_activity_status, f"Playwright: Navegando a {url}") + await page.goto(url, timeout=90000, wait_until='domcontentloaded') + + + # 3. ACEPTAR COOKIES 🍪 + try: + cookie_acceptor_selector = '#sp-cc-accept' + await page.wait_for_selector(cookie_acceptor_selector, timeout=10000) + await page.click(cookie_acceptor_selector) + except Exception: + pass + + # 4. ESPERA FIJA Y CONFIRMACIÓN + root_app.after(0, root_app.update_activity_status, "Playwright: Espera forzada (10s) para carga dinámica...") + await page.wait_for_timeout(10000) + + await page.wait_for_selector('#search', timeout=5000) + + root_app.after(0, root_app.update_activity_status, "Playwright: Contenido cargado. Extrayendo datos...") + content = await page.content() + await browser.close() + + # --- DEPURACIÓN: GUARDAR EL HTML CARGADO --- + with open(DEBUG_HTML_FILE, 'w', encoding='utf-8') as f: + f.write(content) + # ------------------------------------------- + + # --- COMIENZA EL PARSEO CON EXTRACCIÓN TOLERANTE --- + soup = BeautifulSoup(content, 'html.parser') + # Contenedor de producto estable + product_listings = soup.find_all('div', attrs={'data-component-type': 's-search-result'}) + + if not product_listings: + return [{"error": f"No se encontraron listados. El contenedor 's-search-result' no fue encontrado. Posible cambio de selector principal."}] + + for listing in product_listings: + try: + # 1. Nombre del Producto (NOMBRE) + # Buscamos el h2 que contiene el título. + title_h2_tag = listing.find('h2') + # Luego buscamos el primer span dentro de él (que es el texto del título) + nombre = title_h2_tag.find('span').get_text(strip=True) if title_h2_tag and title_h2_tag.find('span') else "N/D" + + # 2. Precio (PRECIO) + # Buscamos en el área de precio y luego buscamos el span a-offscreen (el precio real) + price_container = listing.find('span', class_='a-price') + precio_tag = price_container.find('span', class_='a-offscreen') if price_container else None + precio = precio_tag.get_text(strip=True) if precio_tag else "N/D" + + # 3. Vendedor/Marca (VENDEDOR) + # Buscamos una etiqueta que contenga la palabra 'marca' o 'vendido por' + vendedor = "Marca/Vendedor (N/D)" + # Intentamos usar el aria-label si está disponible, es la fuente más limpia + h2_tag_for_vendor = listing.find('h2') + if h2_tag_for_vendor and 'aria-label' in h2_tag_for_vendor.attrs: + vendedor = h2_tag_for_vendor['aria-label'].split(',')[0] + + # 4. URL de la Imagen (IMAGEN_URL) + image_tag = listing.find('img', class_='s-image') + imagen_url = image_tag['src'] if image_tag and 'src' in image_tag.attrs else "No URL de imagen" + + if nombre == "N/D" or precio == "N/D" or 'Sponsored' in nombre: + continue + + product_data.append({ + "nombre": nombre, + "precio": precio, + "vendedor": vendedor, + "imagen_url": imagen_url + }) + except Exception as e: + # Este error es normal si el listado es un anuncio o elemento atípico. + # El log de errores solo aparece en la consola de depuración. + # print(f"Error al procesar listado de producto: {e}") + pass # Omitir listados que causan error + + # Guardar en JSON y mostrar resultados + with open(SCRAPING_FILE, 'w', encoding='utf-8') as f: + json.dump(product_data, f, indent=4, ensure_ascii=False) + + root_app.after(0, root_app._display_scraping_results, product_data, search_term) + + except Exception as e: + error_message = f"Error crítico durante el scraping: {e}" + root_app.after(0, root_app.update_activity_status, error_message) + root_app.after(0, root_app._display_scraping_results, [{"error": error_message}], search_term) + + +def start_playwright_scraper(search_term, root_app): + """ + Lanzador principal que crea un hilo de Python para ejecutar Playwright. + """ + threading.Thread(target=run_scraper_async, args=(search_term, root_app), daemon=True).start() \ No newline at end of file