commit 401fe152e66a59fbd6040ed2f8fb45fc282a1bdb Author: Luka Date: Sat Dec 6 19:09:50 2025 +0100 Version FInal diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..79a5ec5 Binary files /dev/null and b/__pycache__/config.cpython-312.pyc differ diff --git a/__pycache__/monitor_manager.cpython-312.pyc b/__pycache__/monitor_manager.cpython-312.pyc new file mode 100644 index 0000000..148f890 Binary files /dev/null and b/__pycache__/monitor_manager.cpython-312.pyc differ diff --git a/__pycache__/system_utils.cpython-312.pyc b/__pycache__/system_utils.cpython-312.pyc new file mode 100644 index 0000000..d8de14b Binary files /dev/null and b/__pycache__/system_utils.cpython-312.pyc differ diff --git a/__pycache__/ui_layout.cpython-312.pyc b/__pycache__/ui_layout.cpython-312.pyc new file mode 100644 index 0000000..8463c50 Binary files /dev/null and b/__pycache__/ui_layout.cpython-312.pyc differ diff --git a/backup_script.sh b/backup_script.sh new file mode 100755 index 0000000..6ca64b6 --- /dev/null +++ b/backup_script.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# $1 es la ruta absoluta al directorio del script de Python, pasada desde el código +BASE_DIR="$1" + +# --- CONFIGURACIÓN CON RUTAS ABSOLUTAS --- +# Usamos la ruta base para construir las rutas de origen y destino +SOURCE_DIR="${BASE_DIR}/data" +DEST_DIR="${BASE_DIR}/Copias_Backup/$(date +\%Y-\%m-\%d_\%H-\%M)" +# ------------------------------------------ + +echo "--- Iniciando copia de seguridad de: ${SOURCE_DIR} ---" + +# Crear el directorio de destino +mkdir -p "$DEST_DIR" + +# Verificar si el directorio de origen existe usando la ruta absoluta +if [ -d "$SOURCE_DIR" ]; then + + echo "Copiando a: $DEST_DIR" + # Copiar el contenido de origen al destino. + # Usamos $SOURCE_DIR/* para copiar el contenido y no el directorio 'data' en sí mismo + cp -rv "$SOURCE_DIR" "$DEST_DIR/" + + if [ $? -eq 0 ]; then + echo "La copia de seguridad se ha completado con éxito." + exit 0 + else + echo "ERROR: Fallo al copiar archivos." + exit 1 + fi +else + echo "ERROR: El directorio de origen (${SOURCE_DIR}) no existe. Abortando." + exit 1 +fi \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..5d90e03 --- /dev/null +++ b/config.py @@ -0,0 +1,79 @@ +# config.py +import os +import psutil + +# --- Rutas y Archivos --- +SCRIPT_NAME = "backup_script.sh" +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +SCRIPT_PATH = os.path.join(BASE_DIR, SCRIPT_NAME) +archivo_registro_csv = os.path.join(BASE_DIR, "data", "registro_recursos.csv") + +# NUEVO: Ruta de guardado de JSON y Carpeta de alarmas +ALARM_SAVE_FILE = os.path.join(BASE_DIR, "data", "alarmas.json") +ALARM_FOLDER = os.path.join(BASE_DIR, "data", "alarmas") +ALERTA_SOUND_FILE = None # Almacenará la ruta del archivo de sonido seleccionado por el usuario + +# Rutas de Scraping +SCRAPING_FOLDER = os.path.join(BASE_DIR, "data", "scraping") +SCRAPING_CONFIG_FOLDER = os.path.join(BASE_DIR, "data", "tipo_scraping") + +# NUEVO: Carpeta específica para el Bloc de Notas +NOTES_FOLDER = os.path.join(BASE_DIR, "data", "notas") + +# --- Variables de Monitoreo --- +MAX_PUNTOS = 30 +tiempos = list(range(-MAX_PUNTOS + 1, 1)) +num_cores = psutil.cpu_count(logical=True) +datos_cores = [0] * num_cores + +# --- Datos Dinámicos (Inicialización) --- +datos_cpu = [0] * MAX_PUNTOS +datos_mem = [0] * MAX_PUNTOS +datos_net_sent = [0] * MAX_PUNTOS +datos_net_recv = [0] * MAX_PUNTOS +datos_disk_read = [0] * MAX_PUNTOS +datos_disk_write = [0] * MAX_PUNTOS + +# --- Variables de Estado y UI --- +monitor_running = True +registro_csv_activo = False +system_log = None +progress_bar = None +editor_texto = None +scraping_progress_bar = None # Barra de progreso de scraping +scraping_output_text = None # Área de texto de salida de scraping +scraping_url_input = None # Variable de control de la URL de scraping +scraping_selector_input = None # Entrada para el selector CSS +scraping_attr_input = None # Entrada para el atributo CSS +scraping_config_file_label = None # Label para mostrar el archivo de configuración cargado +scraping_config_data = {} # Diccionario para almacenar la configuración JSON de scraping +scraping_running = False # Bandera de estado de ejecución de scraping + +# Variables de Alarma +alarmas_programadas = {} +alarma_counter = 0 + +# Control de Sonido +alarma_volumen = 0.5 +alarma_sonando = False + +# Control de Juegos (NUEVO) +juego_window = None # Referencia a la ventana Toplevel del juego +juego_running = False # Bandera de estado del juego + +# Control de Música Adicional (NUEVO) +current_music_file = None +music_sonando = False + +# Variables de UI +label_hostname = None +label_os_info = None +label_cpu_model = None +label_ram_total = None +label_disk_total = None +label_net_info = None +label_uptime = None + +label_1 = None # Estado Backup +label_2 = None # Estado Registro CSV +label_fecha_hora = None \ No newline at end of file diff --git a/data/alarmas.json b/data/alarmas.json new file mode 100644 index 0000000..70fd97c --- /dev/null +++ b/data/alarmas.json @@ -0,0 +1,17 @@ +{ + "last_id": 2, + "alarms": { + "1": { + "time_str": "2025-11-29T16:07:00", + "active": false, + "message": "Alarma de prueba", + "sound_file": "/home/luka/Documentos/FP_DAM/Segundo/PSP/Proyecto/data/alarmas/YOUR-PHONE-LINGING.wav" + }, + "2": { + "time_str": "2025-11-29T18:04:00", + "active": false, + "message": "Alarma sin descripci\u00f3n", + "sound_file": "/home/luka/Documentos/FP_DAM/Segundo/PSP/Proyecto/data/alarmas/YOUR-PHONE-LINGING.wav" + } + } +} \ No newline at end of file diff --git a/data/alarmas/YOUR-PHONE-LINGING.wav b/data/alarmas/YOUR-PHONE-LINGING.wav new file mode 100644 index 0000000..0a3be08 Binary files /dev/null and b/data/alarmas/YOUR-PHONE-LINGING.wav differ diff --git a/data/musica/fantasmaBailando.mp3 b/data/musica/fantasmaBailando.mp3 new file mode 100644 index 0000000..5dc0bef Binary files /dev/null and b/data/musica/fantasmaBailando.mp3 differ diff --git a/data/notas/notaEjemplo1.txt b/data/notas/notaEjemplo1.txt new file mode 100644 index 0000000..34a68ad --- /dev/null +++ b/data/notas/notaEjemplo1.txt @@ -0,0 +1 @@ +Mireya Serrano es una crack, texto de ejemplo diff --git a/data/registro_recursos.csv b/data/registro_recursos.csv new file mode 100644 index 0000000..79961de --- /dev/null +++ b/data/registro_recursos.csv @@ -0,0 +1,9 @@ +Timestamp,CPU_Total (%),RAM_Total (%),Net_Sent (KB/s),Net_Recv (KB/s) +2025-11-29 14:04:14,14.1,18.3,104.3623046875,80.29296875 +2025-11-29 14:04:15,17.3,18.4,103.626953125,63.369140625 +2025-11-29 14:04:16,17.5,18.4,97.59765625,285.9462890625 +2025-11-29 14:04:18,14.8,18.4,86.7509765625,6.6787109375 +2025-11-29 14:04:19,15.0,18.4,148.703125,6.470703125 +2025-11-29 14:04:20,13.8,18.4,69.0546875,116.576171875 +2025-11-29 14:04:21,15.7,18.5,89.3642578125,68.330078125 +2025-11-29 14:04:23,13.3,18.5,104.0947265625,62.2880859375 diff --git a/data/scraping/Portatiles_gamer_20251129.txt b/data/scraping/Portatiles_gamer_20251129.txt new file mode 100644 index 0000000..57fc3b7 --- /dev/null +++ b/data/scraping/Portatiles_gamer_20251129.txt @@ -0,0 +1,373 @@ +--- 92 CONTENEDORES DE PRODUCTO ENCONTRADOS --- + +[1] TÍTULO: N/A + PRECIO: 68,€ + ENLACE: N/A +--------------------------------------- +[2] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[3] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[4] TÍTULO: 579,00 €579,00€Recomendado: 849,00 €Recomendado:849,00 €849,00€ + PRECIO: 579,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfYXRmOjMwMDcxODg3NjYxMTUzMjo6MDo6&url=%2FDell-Inspiron-5645-Ordenador-Port%25C3%25A1til%2Fdp%2FB0D22CX72L%2Fref%3Dsr_1_1_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-1-spons%26aref%3DbKVlknqSjv%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1&aref=bKVlknqSjv&sp_cr=ZAZ +--------------------------------------- +[5] TÍTULO: 649,00 €649,00€ + PRECIO: 649,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfYXRmOjMwMDgyNTM4NjI3NTkzMjo6MDo6&url=%2FACER-Nitro-ANV15-51-i5-13420H-Operativo%2Fdp%2FB0CFFF13RK%2Fref%3Dsr_1_2_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-2-spons%26aref%3DZZ2mrjAIrC%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1&aref=ZZ2mrjAIrC&sp_cr=ZAZ +--------------------------------------- +[6] TÍTULO: 529,00 €529,00€Mediano: 579,00 €Mediano:579,00 €579,00€ + PRECIO: 529,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfYXRmOjMwMDgyMDQ0ODI0MTYzMjo6MDo6&url=%2FASUS-Vivobook-F1504VA-NJ2402-Ordenador-Operativo%2Fdp%2FB0DPG9PV5J%2Fref%3Dsr_1_3_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-3-spons%26aref%3DQDjyx1A9u2%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9hdGY%26psc%3D1&aref=QDjyx1A9u2&sp_cr=ZAZ +--------------------------------------- +[7] TÍTULO: 599,00 €599,00€Mediano: 649,00 €Mediano:649,00 €649,00€ + PRECIO: 599,€ + ENLACE: https://www.amazon.es/ASUS-V16-V3607VU-RP148-Ordenador-Operativo/dp/B0DVT8ZZVX/ref=sr_1_4?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-4 +--------------------------------------- +[8] TÍTULO: 549,00 €549,00€Mediano: 599,00 €Mediano:599,00 €599,00€ + PRECIO: 549,€ + ENLACE: https://www.amazon.es/MSI-Laptop-B12UCX-1682XES-i5-12450H-GeForce/dp/B0DBHSG8J7/ref=sr_1_5?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-5 +--------------------------------------- +[9] TÍTULO: 1.199,00 €1.199,00€Recomendado: 1.499,00 €Recomendado:1.499,00 €1.499,00€ + PRECIO: 1.199,€ + ENLACE: https://www.amazon.es/ASUS-TUF-Gaming-A16-FA608UM-RV005/dp/B0F9YYBF45/ref=sr_1_6?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-6 +--------------------------------------- +[10] TÍTULO: 599,00 €599,00€Mediano: 649,00 €Mediano:649,00 €649,00€ + PRECIO: 599,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToyNTA3MDMyOTQ3NDc4MzUyOjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljOjMwMDY2NjgwNTUwMjkzMjo6MDo6&url=%2FASUS-V16-V3607VU-RP148-Ordenador-Operativo%2Fdp%2FB0DVT8ZZVX%2Fref%3Dsxin_14_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%253Aamzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0DVT8ZZVX%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DrmsEN%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Dab227af4-5913-464b-a778-46bbea7f7366%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-1-8dea6190-c31e-4232-8cab-22bcf32666ff-spons%26aref%3DGJr73f1tXz%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=GJr73f1tXz&sp_cr=ZAZ +--------------------------------------- +[11] TÍTULO: 599,00 €599,00€Mediano: 649,00 €Mediano:649,00 €649,00€ + PRECIO: 599,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToyNTA3MDMyOTQ3NDc4MzUyOjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljOjMwMDY2NjgwNTUwMjkzMjo6MDo6&url=%2FASUS-V16-V3607VU-RP148-Ordenador-Operativo%2Fdp%2FB0DVT8ZZVX%2Fref%3Dsxin_14_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%253Aamzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0DVT8ZZVX%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DrmsEN%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Dab227af4-5913-464b-a778-46bbea7f7366%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-1-8dea6190-c31e-4232-8cab-22bcf32666ff-spons%26aref%3DGJr73f1tXz%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=GJr73f1tXz&sp_cr=ZAZ +--------------------------------------- +[12] TÍTULO: 999,00 €999,00€Recomendado: 1.349,00 €Recomendado:1.349,00 €1.349,00€ + PRECIO: 999,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToyNTA3MDMyOTQ3NDc4MzUyOjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljOjMwMDgwOTg1OTY1NzgzMjo6MTo6&url=%2FAlienware-Port%25C3%25A1til-AC16250-GeForce-retroiluminado%2Fdp%2FB0F9B66J9B%2Fref%3Dsxin_14_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%253Aamzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0F9B66J9B%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DrmsEN%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Dab227af4-5913-464b-a778-46bbea7f7366%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-2-8dea6190-c31e-4232-8cab-22bcf32666ff-spons%26aref%3DnWQC5QKxnG%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=nWQC5QKxnG&sp_cr=ZAZ +--------------------------------------- +[13] TÍTULO: 579,00 €579,00€Recomendado: 849,00 €Recomendado:849,00 €849,00€ + PRECIO: 579,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToyNTA3MDMyOTQ3NDc4MzUyOjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljOjMwMDcxODg3NjYxMTUzMjo6Mjo6&url=%2FDell-Inspiron-5645-Ordenador-Port%25C3%25A1til%2Fdp%2FB0D22CX72L%2Fref%3Dsxin_14_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%253Aamzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0D22CX72L%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DrmsEN%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Dab227af4-5913-464b-a778-46bbea7f7366%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-3-8dea6190-c31e-4232-8cab-22bcf32666ff-spons%26aref%3DbKVlknqSjv%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=bKVlknqSjv&sp_cr=ZAZ +--------------------------------------- +[14] TÍTULO: 519,00 €519,00€Recomendado: 699,00 €Recomendado:699,00 €699,00€ + PRECIO: 519,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToyNTA3MDMyOTQ3NDc4MzUyOjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljOjMwMDgyNTQ1NTQzMzYzMjo6Mzo6&url=%2FA17-51M-52VS-Ordenador-Port%25C3%25A1til-Graphics-Windows%2Fdp%2FB0DQ8R71V6%2Fref%3Dsxin_14_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%253Aamzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0DQ8R71V6%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DrmsEN%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Dab227af4-5913-464b-a778-46bbea7f7366%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-4-8dea6190-c31e-4232-8cab-22bcf32666ff-spons%26aref%3Dr1nZmQaK3C%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=r1nZmQaK3C&sp_cr=ZAZ +--------------------------------------- +[15] TÍTULO: 2.499,00 €2.499,00€Recomendado: 2.999,00 €Recomendado:2.999,00 €2.999,00€ + PRECIO: 2.499,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToyNTA3MDMyOTQ3NDc4MzUyOjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljOjMwMDgyNTM5NzYwODMzMjo6NDo6&url=%2FHP-OMEN-MAX-16-ah0010ns-Ordenador%2Fdp%2FB0DT4J3RP2%2Fref%3Dsxin_14_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%253Aamzn1.sym.ab227af4-5913-464b-a778-46bbea7f7366%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0DT4J3RP2%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DrmsEN%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Dab227af4-5913-464b-a778-46bbea7f7366%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-5-8dea6190-c31e-4232-8cab-22bcf32666ff-spons%26aref%3D8iUF6J92HQ%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=8iUF6J92HQ&sp_cr=ZAZ +--------------------------------------- +[16] TÍTULO: 599,00 €599,00€Mediano: 649,00 €Mediano:649,00 €649,00€ + PRECIO: 599,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToxNDYyNTU1MjUzMTMwNzI6MTc2NDQzNTkzNzpzcF9zZWFyY2hfdGhlbWF0aWM6MzAwNjY2ODA1NTAyOTMyOjowOjo&url=%2FASUS-V16-V3607VU-RP148-Ordenador-Operativo%2Fdp%2FB0DVT8ZZVX%2Fref%3Dsxin_15_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%253Aamzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0DVT8ZZVX%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DgZy22%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Df63f2231-3269-4d5f-9cad-1a30362f32b5%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-1-e5bd47e4-f7c4-453b-867b-8c113095610c-spons%26aref%3DGJr73f1tXz%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=GJr73f1tXz&sp_cr=ZAZ +--------------------------------------- +[17] TÍTULO: 599,00 €599,00€Mediano: 649,00 €Mediano:649,00 €649,00€ + PRECIO: 599,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToxNDYyNTU1MjUzMTMwNzI6MTc2NDQzNTkzNzpzcF9zZWFyY2hfdGhlbWF0aWM6MzAwNjY2ODA1NTAyOTMyOjowOjo&url=%2FASUS-V16-V3607VU-RP148-Ordenador-Operativo%2Fdp%2FB0DVT8ZZVX%2Fref%3Dsxin_15_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%253Aamzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0DVT8ZZVX%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DgZy22%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Df63f2231-3269-4d5f-9cad-1a30362f32b5%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-1-e5bd47e4-f7c4-453b-867b-8c113095610c-spons%26aref%3DGJr73f1tXz%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=GJr73f1tXz&sp_cr=ZAZ +--------------------------------------- +[18] TÍTULO: 598,95 €598,95€ + PRECIO: 598,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToxNDYyNTU1MjUzMTMwNzI6MTc2NDQzNTkzNzpzcF9zZWFyY2hfdGhlbWF0aWM6MzAwNTEwMDYwNjYwODMyOjoxOjo&url=%2FMSI-Thin-B12UC-1842XES-Ordenador-i5-12450H%2Fdp%2FB0BSLJX91V%2Fref%3Dsxin_15_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%253Aamzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0BSLJX91V%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DgZy22%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Df63f2231-3269-4d5f-9cad-1a30362f32b5%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-2-e5bd47e4-f7c4-453b-867b-8c113095610c-spons%26aref%3DTN7aBEHaNU%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=TN7aBEHaNU&sp_cr=ZAZ +--------------------------------------- +[19] TÍTULO: 529,99 €529,99€ + PRECIO: 529,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToxNDYyNTU1MjUzMTMwNzI6MTc2NDQzNTkzNzpzcF9zZWFyY2hfdGhlbWF0aWM6MzAwNTk1NDkxMTA1MjMyOjoyOjo&url=%2FFUNYET-15-6-Inch-Procesador-Desbloqueo-Retroiluminado%2Fdp%2FB0FBRWLPHW%2Fref%3Dsxin_15_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%253Aamzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0FBRWLPHW%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DgZy22%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Df63f2231-3269-4d5f-9cad-1a30362f32b5%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-3-e5bd47e4-f7c4-453b-867b-8c113095610c-spons%26aref%3DH3kl90kENO%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=H3kl90kENO&sp_cr=ZAZ +--------------------------------------- +[20] TÍTULO: 419,99 €419,99€Más bajo: 499,99 €Más bajo:499,99 €499,99€ + PRECIO: 419,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToxNDYyNTU1MjUzMTMwNzI6MTc2NDQzNTkzNzpzcF9zZWFyY2hfdGhlbWF0aWM6MzAwODI3OTIwMjg3ODMyOjozOjo&url=%2FACEMAGIC-Port%25C3%25A1til-subprocesos-Retroilluminata-Port%25C3%25A1tiles%2Fdp%2FB0F9KT59MB%2Fref%3Dsxin_15_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%253Aamzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0F9KT59MB%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DgZy22%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Df63f2231-3269-4d5f-9cad-1a30362f32b5%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3D9ZOMT9Jm0JH%252Ft%252BWi68iDSA%253D%253D%26sr%3D1-4-e5bd47e4-f7c4-453b-867b-8c113095610c-spons%26aref%3DFykaBJApcF%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=FykaBJApcF&sp_cr=ZAZ +--------------------------------------- +[21] TÍTULO: 529,00 €529,00€Mediano: 559,00 €Mediano:559,00 €559,00€ + PRECIO: 529,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MToxNDYyNTU1MjUzMTMwNzI6MTc2NDQzNTkzNzpzcF9zZWFyY2hfdGhlbWF0aWM6MzAwNDkxMzY0NjQ1NTMyOjo0Ojo&url=%2FLenovo-IdeaPad-Slim-Gen-Ordenador%2Fdp%2FB0DHXF79F3%2Fref%3Dsxin_15_pa_sp_search_thematic_sspa%3Fcontent-id%3Damzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%253Aamzn1.sym.f63f2231-3269-4d5f-9cad-1a30362f32b5%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0DHXF79F3%26pd_rd_r%3D05b4e9af-f94c-4ae0-827a-8a1c437860b4%26pd_rd_w%3DgZy22%26pd_rd_wg%3DVrHDa%26pf_rd_p%3Df63f2231-3269-4d5f-9cad-1a30362f32b5%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-5-e5bd47e4-f7c4-453b-867b-8c113095610c-spons%26aref%3DZzlHo2BJ5J%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWM%26psc%3D1&aref=ZzlHo2BJ5J&sp_cr=ZAZ +--------------------------------------- +[22] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[23] TÍTULO: 339,99 €339,99€Recomendado: 369,99 €Recomendado:369,99 €369,99€ + PRECIO: 339,€ + ENLACE: https://www.amazon.es/Ordenador-port%C3%A1til-Gamer-Core-incluidos-1920x1200/dp/B0FKMX6T4Y/ref=sr_1_7?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-7 +--------------------------------------- +[24] TÍTULO: 989,00 €989,00€Recomendado: 1.399,00 €Recomendado:1.399,00 €1.399,00€ + PRECIO: 989,€ + ENLACE: https://www.amazon.es/Lenovo-LOQ-Gen-i7-13650HX-Operativo/dp/B0FJ2JTLSX/ref=sr_1_8?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-8 +--------------------------------------- +[25] TÍTULO: 598,95 €598,95€ + PRECIO: 598,€ + ENLACE: https://www.amazon.es/MSI-Thin-B12UC-1842XES-Ordenador-i5-12450H/dp/B0BSLJX91V/ref=sr_1_9?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-9 +--------------------------------------- +[26] TÍTULO: 899,00 €899,00€ + PRECIO: 899,€ + ENLACE: https://www.amazon.es/ANV15-41-R0ZK-Ordenador-Port%C3%A1til-GeForce-Operativo/dp/B0DPHRZS29/ref=sr_1_10?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-10 +--------------------------------------- +[27] TÍTULO: 649,00 €649,00€ + PRECIO: 649,€ + ENLACE: https://www.amazon.es/ACER-Nitro-ANV15-51-i5-13420H-Operativo/dp/B0CFFF13RK/ref=sr_1_11?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-11 +--------------------------------------- +[28] TÍTULO: 474,99 €474,99€Más bajo: 499,99 €Más bajo:499,99 €499,99€ + PRECIO: 474,€ + ENLACE: https://www.amazon.es/FUNYET-15-6-Inch-Procesador-Retroiluminado-Desbloqueo/dp/B0FR4YFNNJ/ref=sr_1_12?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-12 +--------------------------------------- +[29] TÍTULO: 279,99 €279,99€Más bajo: 499,99 €Más bajo:499,99 €499,99€ + PRECIO: 279,€ + ENLACE: https://aax-eu-zaz.amazon.es/x/c/JO5ZkTzGQ67a0nO3SCt7wLsAAAGa0JPHywoAAAH2AQBvbm9fdHhuX2JpZDIgICBvbm9fdHhuX2ltcDIgICBIIsjr/clv1_CEuOPUxokZA0iHrVWP4O_Af3cU0jSqoMuKcl5ReyssRR4X9B81C0OP018HHz9hvVjq0yzpNb3XLTfU0dAmcAWExmDRm0JObpD1s2bR59anj6Aq9hokfB8IkGXCQEUuW6XJeXalC7rdIQEQQ8kV_3ZVhXLA8QC8x8H15tWbp4Xgyay_2WjkwrSxutOI2vKbYuDzUcfMyagcpNBHhtJZhsefQQh8yliMIIKin6O61_zAYOsoJhkeWuV2gvjBmW5lJBN2PMN-SRnR9AjX0lL9xFahCwdXiTzSjwM7ud24z9a7UqLWa11aK1xkuNcA7FBIOT3_cq0eVrbNPflr2DHuWJDe9R6uJMTzUSjZIoyNhqK6dH-mN7IBCo_yWr3JYqfnqDdhxLjmOcuEwDthDmOaGsSFoDf5TFhPBne-wSBml9tlZkUux_rAVrRF9I0bxcInIFH-GzXfr6mO-bWyVNaReipMUTeUmOk2OYfChiJ4snkeH1E39g8pKg8xPpTmFjVYOMr_VkwTthKoxWmj2Ajw-85x_bWgR-Y2A4Ly61XW_0GbfWKhOMGTd0mEAZAg3OfkfrU_0Pj89oQgyJhpZTgsU3XVSZOi98lgp6zP47jZipuW8VvYX617QeS1Ph3Ci9M56RKD4NYvMhETFv6NhuFoJB4AsJz__KACdAemsw4utT2JwaPkQnUimCl3cSgTX8WOl78x4S5GpqzeYl-sgOLXGDSGwA9JvzLcq4gInMF28ZKT6Ogh3NWNdXxCZF0OYXcei5o7hcMhHWpEr38RDcbF7vDQt-ljvlFWEPkdxuVOQLRya75H0oKeVRtBRiopCKXG6ycPWJdy9k6STRuvBQWd5qu7KyolEwzNCLIfitn_6rrUeW32_6wnEZ-wDXtmCqsBn4j-kBu_c5sx3PR-cMefo7Fsj9-dqL5SN1FCc5ND3iohSN1TtyN2YEYUx3fhTH8lSOui-0cuHA7ovqru3MxDCKANVRB8uVabH032KqUQ1_A54OWxwlF0N4Sr3J6Q_rIkw3AT-N86hQ1AtreLMSrLys4SRyiyBsE9-qm1dI4ZYnhx8HDn-7ediaCyjlToJXPvBse3mBfZfJIQMlEo-Wd4axoMriURzh2IlvJJI5MSV-NIiiDEaIJrhYondYDf6QptrDnswpdKH9qwwDGeFJh0QNO2sYUfvuAkr2M9cqmp3zjomhtRGB9HbN0-sAhE90fu0rCXKWKEPi6zisWBQ8uVz6FMKY2_J4sZd03DGJ3oxHC2_ZNlsyfEO8xzmBvjkLe5fTWo4dlPecXSGXbZ5gWpItlFMi_ZtvhKO9gBugqXWpNcDcKelUGNzq7_d-nhOO1r0ESoYCJQmN8W72gcMqR1MG5Jrv54cfw0xqfZhnDsIKiKowyVeSKEm85uusP1awZrw4OQ7aD325SBXYSBMsZI-HNKWEjkK6LFdw-_E5-ePOhWB4fQNGz7coPM8pHwTfNA763z8wAGDhj4mgjwfvN3bVZ0_H1DWEQ-Vo8MQjnc4GyGg_q1qVO1ytNV597oZfh9iirGDCZhdLf-gxNJuGZMUOOhC-mvKXMWa-AWiJd7hAupy96fzLz7W9Q6d9AvosqP9pR8P0NivuyM-gF8bQfxs_Ph1p18nt67ED_4b_2XDVw_y0b1vHCw2krRJRi3ZHGaKBWzPzNkPX8rgkDnbWsSJE45MNehXEZ0R8APJ6iN0_ZKuTC-4KpI7LRFA3z_fgofGHuPl-d5OfvSRNuGW15IYbEyPz-SMPsQCbt4Z7AwyZ41qoitCByts3Urhu_EbqMeYPtboqzybtmI9eNdJlR91O9nN2EdjXw1_jUPSBICr7l5S1OpGo3poS4_Anc_6Kv_FIkRIua91WG88f5sHIFHjPCorNkb54WPCcaulpVjFFvSBBu-jpG52OjSYMSPfb4-lUy_bqQnB2g5Hy-jxaaRfQEsyjnR5vpNo7dab-jqQqpkYBNSQx4rjpAPp90zWYTD1BpZ7fEncVG-e66a-Fc4iWH2T8ovxhN1-9aO4wEl3DUYlq65eZ4Us0yAm2_fFCL87nxvgL-5XUng_gixjIGRwkXbs74vwXOsK9BK9rPcEiGv9XzMYcalcXJPYzxGkkXMnR8nYDd0ZgB475PHyADD33NhpmC422wtnCOa5MjkwSEL6oD991Dm2UbbExjdsnugS5UCQsgWFzbjkeRSefKBju7TXON7mG73YepnHdG5ZQBowrM5PuDffvO8IaGGwdSvBHnGbEK9KGiJ7ps4d9calMx9--vvEIyVV1yt3SHkhv_79RU4siZi_mwYLhcfXGhaRZGHrOHZKKlVfcujpEI581dYPEUZ3r1A9JfrmdJKhMAEKu2MXWbzpxgiNu6tHKEW9CAvOW/https://www.amazon.es/Ruzava-Ordenador-Port%C3%A1til-Expansi%C3%B3n-Inal%C3%A1mbrico/dp/B0FW45B6GC/ref=sxin_24_sbv_search_btf?content-id=amzn1.sym.67f2d338-1537-46df-95f7-c25e03a92a27%3Aamzn1.sym.67f2d338-1537-46df-95f7-c25e03a92a27&cv_ct_cx=port%C3%A1til+gamer&keywords=port%C3%A1til+gamer&pd_rd_i=B0FW45B6GC&pd_rd_r=05b4e9af-f94c-4ae0-827a-8a1c437860b4&pd_rd_w=pQJ6k&pd_rd_wg=VrHDa&pf_rd_p=67f2d338-1537-46df-95f7-c25e03a92a27&pf_rd_r=T9H31YE727AHMGDXPHA3&qid=1764435937&sbo=9ZOMT9Jm0JH%2Ft%2BWi68iDSA%3D%3D&sr=1-1-07652b71-81e3-41f8-9097-e46726928fb7 +--------------------------------------- +[30] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[31] TÍTULO: 229,99 €229,99€ + PRECIO: 229,€ + ENLACE: https://www.amazon.es/pulgadas-Ordenador-N4000-Bluetooth-Estudiantes/dp/B0FFSWPPGP/ref=sr_1_13?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-13 +--------------------------------------- +[32] TÍTULO: 339,99 €339,99€ + PRECIO: 339,€ + ENLACE: https://www.amazon.es/Ordenador-Port%C3%A1til-procesador-Bluetooth-retroiluminado/dp/B0FMFFQW63/ref=sr_1_14?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-14 +--------------------------------------- +[33] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[34] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[35] TÍTULO: 599,00 €599,00€Mediano: 649,00 €Mediano:649,00 €649,00€ + PRECIO: 599,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfbXRmOjMwMDY2NjgwNTUwMjkzMjo6MDo6&url=%2FASUS-V16-V3607VU-RP148-Ordenador-Operativo%2Fdp%2FB0DVT8ZZVX%2Fref%3Dsr_1_17_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-17-spons%26aref%3DGJr73f1tXz%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1&aref=GJr73f1tXz&sp_cr=ZAZ +--------------------------------------- +[36] TÍTULO: 598,95 €598,95€ + PRECIO: 598,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfbXRmOjMwMDUxMDA2MDY2MDgzMjo6MDo6&url=%2FMSI-Thin-B12UC-1842XES-Ordenador-i5-12450H%2Fdp%2FB0BSLJX91V%2Fref%3Dsr_1_18_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-18-spons%26aref%3DTN7aBEHaNU%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1&aref=TN7aBEHaNU&sp_cr=ZAZ +--------------------------------------- +[37] TÍTULO: 719,00 €719,00€Recomendado: 899,00 €Recomendado:899,00 €899,00€ + PRECIO: 719,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfbXRmOjMwMDgyNTM4NjI3NjEzMjo6MDo6&url=%2FLenovo-LOQ-Gen-Ordenador-i5-12450HX%2Fdp%2FB0F6YBR1GC%2Fref%3Dsr_1_19_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-19-spons%26aref%3DEcs9l6WB4E%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1&aref=Ecs9l6WB4E&sp_cr=ZAZ +--------------------------------------- +[38] TÍTULO: 999,00 €999,00€Recomendado: 1.349,00 €Recomendado:1.349,00 €1.349,00€ + PRECIO: 999,€ + ENLACE: https://www.amazon.es/Alienware-Port%C3%A1til-AC16250-GeForce-retroiluminado/dp/B0F9B66J9B/ref=sr_1_20?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-20 +--------------------------------------- +[39] TÍTULO: 759,00 €759,00€Mediano: 799,00 €Mediano:799,00 €799,00€ + PRECIO: 759,€ + ENLACE: https://www.amazon.es/HP-Victus-15-fb3035ns-Ordenador-port%C3%A1til/dp/B0F2TM3YKB/ref=sr_1_21?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-21 +--------------------------------------- +[40] TÍTULO: 1.599,00 €1.599,00€ + PRECIO: 1.599,€ + ENLACE: https://www.amazon.es/HP-OMEN-Ordenador-port%C3%A1til-FreeDos/dp/B0FM8DMJQ2/ref=sr_1_22?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-22 +--------------------------------------- +[41] TÍTULO: 549,99 €549,99€ + PRECIO: 549,€ + ENLACE: https://www.amazon.es/Tivique-Ordenador-Port%C3%A1til-Pulgadas-Ampliable/dp/B0FJWWSZ5J/ref=sr_1_23?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-23 +--------------------------------------- +[42] TÍTULO: 719,00 €719,00€Recomendado: 899,00 €Recomendado:899,00 €899,00€ + PRECIO: 719,€ + ENLACE: https://www.amazon.es/Lenovo-LOQ-Gen-Ordenador-i5-12450HX/dp/B0F6YBR1GC/ref=sr_1_24?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-24 +--------------------------------------- +[43] TÍTULO: 899,00 €899,00€ + PRECIO: 899,€ + ENLACE: https://www.amazon.es/Lenovo-LOQ-Gen-Ordenador-Operativo/dp/B0DTJ3B9R9/ref=sr_1_25?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-25 +--------------------------------------- +[44] TÍTULO: 512,94 €512,94€Más bajo: 539,94 €Más bajo:539,94 €539,94€ + PRECIO: 512,€ + ENLACE: https://www.amazon.es/Ordenador-Port%C3%A1til-Pantalla-Pulgadas-Retroiluminado/dp/B0FJX8SHHX/ref=sr_1_26?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-26 +--------------------------------------- +[45] TÍTULO: 1.399,99 €1.399,99€Recomendado: 1.699,00 €Recomendado:1.699,00 €1.699,00€ + PRECIO: 1.399,€ + ENLACE: https://www.amazon.es/HP-OMEN-16-am0033ns-Ordenador-port%C3%A1til/dp/B0F2TKFPYC/ref=sr_1_27?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-27 +--------------------------------------- +[46] TÍTULO: 370,49 €370,49€Más bajo: 499,95 €Más bajo:499,95 €499,95€ + PRECIO: 370,€ + ENLACE: https://www.amazon.es/Tivique-Ordenador-Expansi%C3%B3n-Procesador-Leichtgewicht/dp/B0FC6B3KGH/ref=sr_1_28?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-28 +--------------------------------------- +[47] TÍTULO: 999,00 €999,00€ + PRECIO: 999,€ + ENLACE: https://www.amazon.es/HP-Victus-15-fa2022ns-Ordenador-i7-13620H/dp/B0F2TJCB7B/ref=sr_1_29?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-29 +--------------------------------------- +[48] TÍTULO: 1.799,00 €1.799,00€ + PRECIO: 1.799,€ + ENLACE: https://www.amazon.es/HP-OMEN-Ordenador-port%C3%A1til-9-8940HX/dp/B0FM8D7GJK/ref=sr_1_30?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-30 +--------------------------------------- +[49] TÍTULO: 379,99 €379,99€Más bajo: 499,99 €Más bajo:499,99 €499,99€ + PRECIO: 379,€ + ENLACE: https://www.amazon.es/FUNYET-Discretos-Procesador-1920x1080-Retroiluminado/dp/B0FXHKFKXQ/ref=sr_1_31?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-31 +--------------------------------------- +[50] TÍTULO: 979,00 €979,00€ + PRECIO: 979,€ + ENLACE: https://www.amazon.es/GIGABYTE-Port%C3%A1til-Gaming-i7-13700H-MF-72ES893KD/dp/B0F22B43LF/ref=sr_1_32?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-32 +--------------------------------------- +[51] TÍTULO: 599,00 €599,00€Mediano: 648,95 €Mediano:648,95 €648,95€ + PRECIO: 599,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfbXRmOjMwMDgxMDA0MDMxNDIzMjo6MDo6&url=%2FMSI-F13MG-236XES-port%25C3%25A1til-Profesional-1920x1080%2Fdp%2FB0DKX3WHTX%2Fref%3Dsr_1_33_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-33-spons%26aref%3DekOkwahkVK%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1&aref=ekOkwahkVK&sp_cr=ZAZ +--------------------------------------- +[52] TÍTULO: 1.982,64 €1.982,64€ + PRECIO: 1.982,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfbXRmOjMwMDY2Njc3ODc5NTMzMjo6MDo6&url=%2FASUS-ROG-Strix-G18-G815LP-S9004%2Fdp%2FB0DMNY618Q%2Fref%3Dsr_1_34_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-34-spons%26aref%3DvmTIMsoi7r%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1&aref=vmTIMsoi7r&sp_cr=ZAZ +--------------------------------------- +[53] TÍTULO: 999,00 €999,00€ + PRECIO: 999,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfbXRmOjMwMDgyNTMzMjUwMzQzMjo6MDo6&url=%2FHP-Victus-15-fa2004ns-Ordenador-port%25C3%25A1til%2Fdp%2FB0F2TK7LTG%2Fref%3Dsr_1_35_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-35-spons%26aref%3DboWHoM2LdQ%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1&aref=boWHoM2LdQ&sp_cr=ZAZ +--------------------------------------- +[54] TÍTULO: 519,00 €519,00€Recomendado: 699,00 €Recomendado:699,00 €699,00€ + PRECIO: 519,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfbXRmOjMwMDgyNTQ1NTQzMzYzMjo6MDo6&url=%2FA17-51M-52VS-Ordenador-Port%25C3%25A1til-Graphics-Windows%2Fdp%2FB0DQ8R71V6%2Fref%3Dsr_1_36_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-36-spons%26aref%3Dr1nZmQaK3C%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9tdGY%26psc%3D1&aref=r1nZmQaK3C&sp_cr=ZAZ +--------------------------------------- +[55] TÍTULO: 284,05 €284,05€Más bajo: 299,00 €Más bajo:299,00 €299,00€ + PRECIO: 284,€ + ENLACE: https://www.amazon.es/Ordenador-computadora-port%C3%A1til16GB-Integrado-Bluetooth/dp/B0F6K86872/ref=sr_1_37?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-37 +--------------------------------------- +[56] TÍTULO: 189,51 €189,51€Más bajo: 199,49 €Más bajo:199,49 €199,49€ + PRECIO: 189,€ + ENLACE: https://www.amazon.es/Upbud-Ordenador-Celeron-N4000-1920x1080/dp/B0FKGFDYB4/ref=sr_1_38?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-38 +--------------------------------------- +[57] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[58] TÍTULO: 269,46 €269,46€Más bajo: 309,96 €Más bajo:309,96 €309,96€ + PRECIO: 269,€ + ENLACE: https://www.amazon.es/BEYNIVAN-N95-1920x1080-Computer-Notebook/dp/B0FMRQ91KM/ref=sr_1_40?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-40 +--------------------------------------- +[59] TÍTULO: 699,99 €699,99€ + PRECIO: 699,€ + ENLACE: https://www.amazon.es/NAIKLULU-Ordenador-port%C3%A1til-Processor-retroiluminado/dp/B0FVMBX9PT/ref=sr_1_41?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-41 +--------------------------------------- +[60] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[61] TÍTULO: 609,99 €609,99€Más bajo: 899,99 €Más bajo:899,99 €899,99€ + PRECIO: 609,€ + ENLACE: https://www.amazon.es/FUNYET-Ordenador-Port%C3%A1til-Retroiluminado-Digitales/dp/B0G2581YCH/ref=sr_1_43?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-43 +--------------------------------------- +[62] TÍTULO: 503,49 €503,49€Más bajo: 569,99 €Más bajo:569,99 €569,99€ + PRECIO: 503,€ + ENLACE: https://www.amazon.es/ACEMAGIC-Ordenador-Portatil-6800H-Retroilluminata/dp/B0DNPXZCHP/ref=sr_1_44?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-44 +--------------------------------------- +[63] TÍTULO: 496,40 €496,40€Mediano: 529,33 €Mediano:529,33 €529,33€ + PRECIO: 496,€ + ENLACE: https://www.amazon.es/Lenovo-IdeaPad-Gaming-Gen-82K201L0PG/dp/B0B2WW81VS/ref=sr_1_45?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-45 +--------------------------------------- +[64] TÍTULO: 3.248,64 €3.248,64€ + PRECIO: 3.248,€ + ENLACE: https://www.amazon.es/Gigabyte-AORUS-Master-16-Port%C3%A1til/dp/B0DZGXX2M3/ref=sr_1_46?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-46 +--------------------------------------- +[65] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[66] TÍTULO: 256,99 €256,99€ + PRECIO: 256,€ + ENLACE: https://www.amazon.es/Ordenador-Celeron-N4000-Computadora-Mini-HDMI/dp/B0FBWHVHG1/ref=sr_1_48?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-48 +--------------------------------------- +[67] TÍTULO: 529,99 €529,99€Más bajo: 569,99 €Más bajo:569,99 €569,99€ + PRECIO: 529,€ + ENLACE: https://www.amazon.es/FUNYET-Ordenador-Port%C3%A1til-Retroiluminado-Digitales/dp/B0G1SSJ56N/ref=sr_1_49?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-49 +--------------------------------------- +[68] TÍTULO: 1.699,00 €1.699,00€Recomendado: 1.999,00 €Recomendado:1.999,00 €1.999,00€ + PRECIO: 1.699,€ + ENLACE: https://www.amazon.es/Lenovo-Port%C3%A1til-i9-14900HX-GeForce-Operativo/dp/B0FJDNDQGF/ref=sr_1_50?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-50 +--------------------------------------- +[69] TÍTULO: 379,49 €379,49€Más bajo: 409,99 €Más bajo:409,99 €409,99€ + PRECIO: 379,€ + ENLACE: https://www.amazon.es/Ordenador-Port%C3%A1til-Pulgadas-Retroiluminado-Mini-HDMI/dp/B0FR47L1TM/ref=sr_1_51?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-51 +--------------------------------------- +[70] TÍTULO: 199,49 €199,49€Más bajo: 209,99 €Más bajo:209,99 €209,99€ + PRECIO: 199,€ + ENLACE: https://www.amazon.es/Ordenador-Port%C3%A1til-Pulgadas-Computadora-Portatiles/dp/B0FP2MW96N/ref=sr_1_52?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-52 +--------------------------------------- +[71] TÍTULO: 206,99 €206,99€Más bajo: 219,99 €Más bajo:219,99 €219,99€ + PRECIO: 206,€ + ENLACE: https://www.amazon.es/Ordenador-Port%C3%A1til-Procesador-Estudiantes-Empresas/dp/B0FPCMLH64/ref=sr_1_53?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-53 +--------------------------------------- +[72] TÍTULO: 265,99 €265,99€Mediano: 279,99 €Mediano:279,99 €279,99€ + PRECIO: 265,€ + ENLACE: https://www.amazon.es/AOC-Ordenador-Portatil-Notebook-Expansi%C3%B3n/dp/B0D8L79YR8/ref=sr_1_54?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-54 +--------------------------------------- +[73] TÍTULO: 522,48 €522,48€Mediano: 549,99 €Mediano:549,99 €549,99€ + PRECIO: 522,€ + ENLACE: https://www.amazon.es/ACEMAGIC-Laptop-Ryzen-6900HX-512GB/dp/B0DK923J77/ref=sr_1_55?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-55 +--------------------------------------- +[74] TÍTULO: 1.299,00 €1.299,00€ + PRECIO: 1.299,€ + ENLACE: https://www.amazon.es/HP-OMEN-16-ap0000ns-Ordenador-port%C3%A1til/dp/B0F2TGCBXR/ref=sr_1_56?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-56 +--------------------------------------- +[75] TÍTULO: 272,49 €272,49€Más bajo: 289,99 €Más bajo:289,99 €289,99€ + PRECIO: 272,€ + ENLACE: https://www.amazon.es/Celeron-N5095-Desbloqueo-Dactilares-Retroiluminado/dp/B0FMRJ32L3/ref=sr_1_57?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-57 +--------------------------------------- +[76] TÍTULO: 529,00 €529,00€Mediano: 559,00 €Mediano:559,00 €559,00€ + PRECIO: 529,€ + ENLACE: https://www.amazon.es/Lenovo-IdeaPad-Slim-Gen-Ordenador/dp/B0DHXF79F3/ref=sr_1_58?dib=eyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q&dib_tag=se&keywords=port%C3%A1til+gamer&qid=1764435937&sr=8-58 +--------------------------------------- +[77] TÍTULO: 999,00 €999,00€Recomendado: 1.349,00 €Recomendado:1.349,00 €1.349,00€ + PRECIO: 999,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfYnRmOjMwMDgwOTg1OTY1NzgzMjo6MDo6&url=%2FAlienware-Port%25C3%25A1til-AC16250-GeForce-retroiluminado%2Fdp%2FB0F9B66J9B%2Fref%3Dsr_1_59_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-59-spons%26aref%3DnWQC5QKxnG%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9idGY%26psc%3D1&aref=nWQC5QKxnG&sp_cr=ZAZ +--------------------------------------- +[78] TÍTULO: 629,00 €629,00€Recomendado: 699,00 €Recomendado:699,00 €699,00€ + PRECIO: 629,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo0NTUzMjI1ODQ5OTA2OTE4OjE3NjQ0MzU5Mzc6c3BfYnRmOjMwMDgyNTUxNzI4NTIzMjo6MDo6&url=%2FAcer-Aspire-Go-15-Ordenador%2Fdp%2FB0DY1GXYTS%2Fref%3Dsr_1_60_sspa%3Fdib%3DeyJ2IjoiMSJ9.gV1B6-h5qKQOZuvFxJGwfs32p05wuKNYyzi_kjUMTg9uBHHrHw25D6q9HknPA8HGxvoEAw9gi9jmYVTCeuLvV5N6MWbUkD44q03RFUtZOypOm8MKVoLig0riRkmU_y2CPydd6CK2Oj6_ptOqptTpf2tDkswHBF4xzXFDjeSxYhCyQMtEUbYwrBM257SVUnjXVzd3gnjxGNr17i7mzwipOVexBdvnoeJOobesJXE54BcnARPnwuGEw69NooxFeKc1lgjr4FTTTDVsUn5QuX9RpORaIIfNMQEVq_LCtN4omZo.pwsPB07z3M3GROtiko4RgWqpoP83V88ZQrEbWKZtw3Q%26dib_tag%3Dse%26keywords%3Dport%25C3%25A1til%2Bgamer%26qid%3D1764435937%26sr%3D8-60-spons%26aref%3DAfAe8VClm1%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9idGY%26psc%3D1&aref=AfAe8VClm1&sp_cr=ZAZ +--------------------------------------- +[79] TÍTULO: 1.499,99 €1.499,99€ + PRECIO: 1.499,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo4OTc0MTQ5NzkzNzkyMDE3OjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljX2J0ZjozMDA4MjgzNzYwOTI4MzI6OjE6Og&url=%2FNIAKUN-Procesador-Computadora-Retroiluminado-1920x1080P%2Fdp%2FB0G1ZCNN29%2Fref%3Dsxbs_pa_sp_search_thematic_btf_sspa%3Fcontent-id%3Damzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%253Aamzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0G1ZCNN29%26pd_rd_r%3Da14eec4d-959f-4f11-9b58-54a7c1502a76%26pd_rd_w%3Dpu7kE%26pd_rd_wg%3DVssi9%26pf_rd_p%3D987cb6fd-8467-4000-857b-32e1177d6a5f%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-1-94703181-497d-44e7-bdd7-504a2b56ccbe-spons%26aref%3DmNaNQqlmtS%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWNfYnRm%26psc%3D1&aref=mNaNQqlmtS&sp_cr=ZAZ +--------------------------------------- +[80] TÍTULO: 1.499,99 €1.499,99€ + PRECIO: 1.499,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo4OTc0MTQ5NzkzNzkyMDE3OjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljX2J0ZjozMDA4MjgzNzYwOTI4MzI6OjE6Og&url=%2FNIAKUN-Procesador-Computadora-Retroiluminado-1920x1080P%2Fdp%2FB0G1ZCNN29%2Fref%3Dsxbs_pa_sp_search_thematic_btf_sspa%3Fcontent-id%3Damzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%253Aamzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0G1ZCNN29%26pd_rd_r%3Da14eec4d-959f-4f11-9b58-54a7c1502a76%26pd_rd_w%3Dpu7kE%26pd_rd_wg%3DVssi9%26pf_rd_p%3D987cb6fd-8467-4000-857b-32e1177d6a5f%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-1-94703181-497d-44e7-bdd7-504a2b56ccbe-spons%26aref%3DmNaNQqlmtS%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWNfYnRm%26psc%3D1&aref=mNaNQqlmtS&sp_cr=ZAZ +--------------------------------------- +[81] TÍTULO: 699,99 €699,99€ + PRECIO: 699,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo4OTc0MTQ5NzkzNzkyMDE3OjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljX2J0ZjozMDA4MjgyNTU1NTc0MzI6OjI6Og&url=%2FNAIKLULU-Ordenador-port%25C3%25A1til-Processor-retroiluminado%2Fdp%2FB0FVMBX9PT%2Fref%3Dsxbs_pa_sp_search_thematic_btf_sspa%3Fcontent-id%3Damzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%253Aamzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0FVMBX9PT%26pd_rd_r%3Da14eec4d-959f-4f11-9b58-54a7c1502a76%26pd_rd_w%3Dpu7kE%26pd_rd_wg%3DVssi9%26pf_rd_p%3D987cb6fd-8467-4000-857b-32e1177d6a5f%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-2-94703181-497d-44e7-bdd7-504a2b56ccbe-spons%26aref%3D74mDLBXECj%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWNfYnRm%26psc%3D1&aref=74mDLBXECj&sp_cr=ZAZ +--------------------------------------- +[82] TÍTULO: 279,99 €279,99€ + PRECIO: 279,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo4OTc0MTQ5NzkzNzkyMDE3OjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljX2J0ZjozMDA4Mjc2MjEzMzY5MzI6OjM6Og&url=%2FRuzava-Ordenador-Port%25C3%25A1til-Expansi%25C3%25B3n-Inal%25C3%25A1mbrico%2Fdp%2FB0FW3NWN64%2Fref%3Dsxbs_pa_sp_search_thematic_btf_sspa%3Fcontent-id%3Damzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%253Aamzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0FW3NWN64%26pd_rd_r%3Da14eec4d-959f-4f11-9b58-54a7c1502a76%26pd_rd_w%3Dpu7kE%26pd_rd_wg%3DVssi9%26pf_rd_p%3D987cb6fd-8467-4000-857b-32e1177d6a5f%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3DRZvfv%252F%252FHxDF%252BO5021pAnSA%253D%253D%26sr%3D1-3-94703181-497d-44e7-bdd7-504a2b56ccbe-spons%26aref%3DiGmMWFjr9S%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWNfYnRm%26psc%3D1&aref=iGmMWFjr9S&sp_cr=ZAZ +--------------------------------------- +[83] TÍTULO: 379,99 €379,99€Más bajo: 399,99 €Más bajo:399,99 €399,99€ + PRECIO: 379,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo4OTc0MTQ5NzkzNzkyMDE3OjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljX2J0ZjozMDA4Mjk0MDYzMDU4MzI6OjQ6Og&url=%2FDIAKUOE-Pulgadas-Ordenador-Port%25C3%25A1til-Retroiluminado%2Fdp%2FB0FXM2PQBK%2Fref%3Dsxbs_pa_sp_search_thematic_btf_sspa%3Fcontent-id%3Damzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%253Aamzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0FXM2PQBK%26pd_rd_r%3Da14eec4d-959f-4f11-9b58-54a7c1502a76%26pd_rd_w%3Dpu7kE%26pd_rd_wg%3DVssi9%26pf_rd_p%3D987cb6fd-8467-4000-857b-32e1177d6a5f%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3D9ZOMT9Jm0JH%252Ft%252BWi68iDSA%253D%253D%26sr%3D1-4-94703181-497d-44e7-bdd7-504a2b56ccbe-spons%26aref%3DIUVnO0j45c%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWNfYnRm%26psc%3D1&aref=IUVnO0j45c&sp_cr=ZAZ +--------------------------------------- +[84] TÍTULO: 539,99 €539,99€Más bajo: 599,99 €Más bajo:599,99 €599,99€ + PRECIO: 539,€ + ENLACE: https://www.amazon.es/sspa/click?ie=UTF8&spc=MTo4OTc0MTQ5NzkzNzkyMDE3OjE3NjQ0MzU5Mzc6c3Bfc2VhcmNoX3RoZW1hdGljX2J0ZjozMDA4Mjk0MDYzMDYwMzI6OjU6Og&url=%2FDIAKUOE-Ordenador-Retroiluminado-Desbloqueo-Dactilares%2Fdp%2FB0FXM2R63Y%2Fref%3Dsxbs_pa_sp_search_thematic_btf_sspa%3Fcontent-id%3Damzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%253Aamzn1.sym.987cb6fd-8467-4000-857b-32e1177d6a5f%26cv_ct_cx%3Dport%25C3%25A1til%2Bgamer%26keywords%3Dport%25C3%25A1til%2Bgamer%26pd_rd_i%3DB0FXM2R63Y%26pd_rd_r%3Da14eec4d-959f-4f11-9b58-54a7c1502a76%26pd_rd_w%3Dpu7kE%26pd_rd_wg%3DVssi9%26pf_rd_p%3D987cb6fd-8467-4000-857b-32e1177d6a5f%26pf_rd_r%3DT9H31YE727AHMGDXPHA3%26qid%3D1764435937%26sbo%3D9ZOMT9Jm0JH%252Ft%252BWi68iDSA%253D%253D%26sr%3D1-5-94703181-497d-44e7-bdd7-504a2b56ccbe-spons%26aref%3Da9mgpDONo5%26sp_csd%3Dd2lkZ2V0TmFtZT1zcF9zZWFyY2hfdGhlbWF0aWNfYnRm%26psc%3D1&aref=a9mgpDONo5&sp_cr=ZAZ +--------------------------------------- +[85] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[86] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[87] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[88] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[89] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[90] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[91] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- +[92] TÍTULO: N/A + PRECIO: Precio No Encontrado + ENLACE: N/A +--------------------------------------- + +--- EXTRACCIÓN FINALIZADA --- + diff --git a/data/scrapping/ScrapingCincuentaFotosDeTeterasAmazon.txt b/data/scrapping/ScrapingCincuentaFotosDeTeterasAmazon.txt new file mode 100644 index 0000000..c2533b8 --- /dev/null +++ b/data/scrapping/ScrapingCincuentaFotosDeTeterasAmazon.txt @@ -0,0 +1,55 @@ +--- 79 ATRIBUTOS 'src' ENCONTRADOS CON SELECTOR: 'div.s-result-item img.s-image' --- + +[1] VALOR: https://m.media-amazon.com/images/I/518N3RrNjIL._AC_UL320_.jpg +[2] VALOR: https://m.media-amazon.com/images/I/71DhdacOlYL._AC_UL320_.jpg +[3] VALOR: https://m.media-amazon.com/images/I/91jwg+syarL._AC_UL320_.jpg +[4] VALOR: https://m.media-amazon.com/images/I/61GOkL4emoL._AC_UL320_.jpg +[5] VALOR: https://m.media-amazon.com/images/I/71FJMRPYsxL._AC_UL320_.jpg +[6] VALOR: https://m.media-amazon.com/images/I/51kjD1v+ZDL._AC_UL320_.jpg +[7] VALOR: https://m.media-amazon.com/images/I/51djNpbBoOL._AC_UL320_.jpg +[8] VALOR: https://m.media-amazon.com/images/I/51aHEyDGW7L._AC_UL320_.jpg +[9] VALOR: https://m.media-amazon.com/images/I/71+03CDi8IL._AC_UL320_.jpg +[10] VALOR: https://m.media-amazon.com/images/I/61s5ttSTtNL._AC_UL320_.jpg +[11] VALOR: https://m.media-amazon.com/images/I/8195dr5uJBL._AC_UL320_.jpg +[12] VALOR: https://m.media-amazon.com/images/I/61ogCRlZswL._AC_UL320_.jpg +[13] VALOR: https://m.media-amazon.com/images/I/71+03CDi8IL._AC_UL320_.jpg +[14] VALOR: https://m.media-amazon.com/images/I/51C6ZkJv7RL._AC_UL320_.jpg +[15] VALOR: https://m.media-amazon.com/images/I/61KFdK3b1XL._AC_UL320_.jpg +[16] VALOR: https://m.media-amazon.com/images/I/51kdF5XygAL._AC_UL320_.jpg +[17] VALOR: https://m.media-amazon.com/images/I/71mH3cC73pL._AC_UL320_.jpg +[18] VALOR: https://m.media-amazon.com/images/I/71XPiTK1oBL._AC_UL640_QL65_.jpg +[19] VALOR: https://m.media-amazon.com/images/I/71XPiTK1oBL._AC_UL640_QL65_.jpg +[20] VALOR: https://m.media-amazon.com/images/I/717a+TavuML._AC_UL320_.jpg +[21] VALOR: https://m.media-amazon.com/images/I/6130QewFv6L._AC_UL320_.jpg +[22] VALOR: https://m.media-amazon.com/images/I/61ogCRlZswL._AC_UL320_.jpg +[23] VALOR: https://m.media-amazon.com/images/I/51RgoXOI1JL._AC_UL320_.jpg +[24] VALOR: https://m.media-amazon.com/images/I/51b0ruoeNYL._AC_UL320_.jpg +[25] VALOR: https://m.media-amazon.com/images/I/71it0pAJQ4L._AC_UL320_.jpg +[26] VALOR: https://m.media-amazon.com/images/I/61MvtmRZKYL._AC_UL320_.jpg +[27] VALOR: https://m.media-amazon.com/images/I/71S3neiV4SL._AC_UL320_.jpg +[28] VALOR: https://m.media-amazon.com/images/I/111mHoVK0kL._SS200_.png +[29] VALOR: https://m.media-amazon.com/images/I/81ecM8ficYL._AC_UL320_.jpg +[30] VALOR: https://m.media-amazon.com/images/I/61wP5sRoMZL._AC_UL320_.jpg +[31] VALOR: https://m.media-amazon.com/images/I/81iPyhIei6L._AC_UL320_.jpg +[32] VALOR: https://m.media-amazon.com/images/I/5187zw-PuPL._AC_UL320_.jpg +[33] VALOR: https://m.media-amazon.com/images/I/51RzoO3IkyL._AC_UL320_.jpg +[34] VALOR: https://m.media-amazon.com/images/I/71OHylUPe+L._AC_UL320_.jpg +[35] VALOR: https://m.media-amazon.com/images/I/51j5J6alaLL._AC_UL320_.jpg +[36] VALOR: https://m.media-amazon.com/images/I/71fKfFjMyXL._AC_UL320_.jpg +[37] VALOR: https://m.media-amazon.com/images/I/61glz0J9BiL._AC_UL320_.jpg +[38] VALOR: https://m.media-amazon.com/images/I/61k9p3lMWeL._AC_UL320_.jpg +[39] VALOR: https://m.media-amazon.com/images/I/71aGgDPgfBL._AC_UL320_.jpg +[40] VALOR: https://m.media-amazon.com/images/I/71wLvos3S5L._AC_UL320_.jpg +[41] VALOR: https://m.media-amazon.com/images/I/61RfE1q-U8L._AC_UL320_.jpg +[42] VALOR: https://m.media-amazon.com/images/I/81oSncDibEL._AC_UL320_.jpg +[43] VALOR: https://m.media-amazon.com/images/I/71it0pAJQ4L._AC_UL320_.jpg +[44] VALOR: https://m.media-amazon.com/images/I/71LE71zDISL._AC_UL320_.jpg +[45] VALOR: https://m.media-amazon.com/images/I/51kjD1v+ZDL._AC_UL320_.jpg +[46] VALOR: https://m.media-amazon.com/images/I/51C6ZkJv7RL._AC_UL320_.jpg +[47] VALOR: https://m.media-amazon.com/images/I/610nfbH4D6L._AC_UL320_.jpg +[48] VALOR: https://m.media-amazon.com/images/I/81mKIzQCfaL._AC_UL320_.jpg +[49] VALOR: https://m.media-amazon.com/images/I/61pXEZmKCvL._AC_UL320_.jpg +[50] VALOR: https://m.media-amazon.com/images/I/61yu1fJ1yqL._AC_UL320_.jpg + +--- EXTRACCIÓN FINALIZADA --- + diff --git a/data/tipo_scraping/amazon_portatiles_gamer.json b/data/tipo_scraping/amazon_portatiles_gamer.json new file mode 100644 index 0000000..8aa9cc2 --- /dev/null +++ b/data/tipo_scraping/amazon_portatiles_gamer.json @@ -0,0 +1,8 @@ +{ + "description": "Extrae título, enlace completo y precio de los portátiles gamer en una sola pasada. Requiere la opción combinada 'Portátiles Gamer (Enlace + Precio)'", + "url": "https://www.amazon.es/s?k=portatil+gamer", + "type": "Portátiles Gamer (Enlace + Precio)", + "selector": "div.s-result-item", + "attribute": null, + "notes": "El selector (div.s-result-item) apunta al contenedor de cada producto. Si falla, Amazon ha cambiado la clase principal del contenedor." +} \ No newline at end of file diff --git a/data/tipo_scraping/amazon_teteras.json b/data/tipo_scraping/amazon_teteras.json new file mode 100644 index 0000000..8900d11 --- /dev/null +++ b/data/tipo_scraping/amazon_teteras.json @@ -0,0 +1,8 @@ +{ + "description": "Busca el enlace de la imagen principal de cada resultado de búsqueda de tetera.", + "url": "https://www.amazon.es/s?k=tetera", + "type": "-> Atributo Específico (CSS Selector + Attr)", + "selector": "div.s-result-item img.s-image", + "attribute": "src", + "notes": "Extrae la URL (src) de las imágenes. Si falla, el selector del contenedor de la imagen ha cambiado." +} \ No newline at end of file diff --git a/monitor_manager.py b/monitor_manager.py new file mode 100644 index 0000000..5c554bd --- /dev/null +++ b/monitor_manager.py @@ -0,0 +1,553 @@ +# 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 ().\n" + + elif tipo_extraccion == "-> Texto Específico (CSS Selector)": + elements = soup.select(selector) + if elements: + result_text += f"--- {len(elements)} ELEMENTOS ENCONTRADOS CON SELECTOR: '{selector}' ---\n\n" + for i, el in enumerate(elements): + text = el.get_text(strip=True) + result_text += f"[{i+1}]: {text[:300]}{'...' if len(text) > 300 else ''}\n\n" + else: + result_text += f"No se encontraron elementos con el selector: '{selector}'.\n" + + elif tipo_extraccion == "-> Atributo Específico (CSS Selector + Attr)": + if not atributo: + result_text += f"ERROR: El modo Atributo requiere un Selector y un Atributo (ej: 'href', 'src').\n" + else: + elements = soup.select(selector) + if elements: + result_text += f"--- {len(elements)} ATRIBUTOS '{atributo}' ENCONTRADOS CON SELECTOR: '{selector}' ---\n\n" + for i, el in enumerate(elements): + attr_value = el.get(atributo, "N/A (Atributo no encontrado)") + result_text += f"[{i+1}] VALOR: {attr_value}\n" + else: + result_text += f"No se encontraron elementos con el selector: '{selector}'.\n" + + result_text += "\n--- EXTRACCIÓN FINALIZADA ---\n" + + # 6. Mostrar el resultado en la UI + if root.winfo_exists() and config.scraping_running: + root.after(0, lambda: output_text_widget.delete('1.0', tk.END)) + root.after(0, lambda: output_text_widget.insert(tk.END, result_text)) + root.after(0, system_utils.log_event, "Scrapear finalizado con éxito.") + + except requests.exceptions.RequestException as e: + error_msg = f"ERROR de Red o HTTP: {e}" + if root.winfo_exists() and config.scraping_running: + root.after(0, lambda: output_text_widget.insert(tk.END, error_msg)) + root.after(0, system_utils.log_event, error_msg) + except Exception as e: + error_msg = f"ERROR inesperado al analizar el contenido: {e}" + if root.winfo_exists() and config.scraping_running: + root.after(0, lambda: output_text_widget.insert(tk.END, error_msg)) + root.after(0, system_utils.log_event, error_msg) + + finally: + # 7. Limpieza final + config.scraping_running = False + if root.winfo_exists(): + root.after(0, progress_bar.stop) + root.after(0, progress_bar.config, {"value": 0}) + + if not root.winfo_exists(): return + root.after(0, system_utils.log_event, f"Estado de Scrapear reseteado. Detenido: {not config.scraping_running}") + + + # Lanzar el hilo de Scrapping + threading.Thread(target=perform_scraping, daemon=True).start() + + +# =============================================== +# Monitoreo del Sistema (Existente) +# =============================================== +def get_top_processes(limit=10): + """Obtiene los N procesos con mayor uso de CPU y sus métricas.""" + processes_list = [] + for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'num_threads']): + try: + mem_info = proc.info['memory_info'] + cpu_percent = proc.info['cpu_percent'] + num_threads = proc.info['num_threads'] + + if cpu_percent is not None and cpu_percent > 0.0: + processes_list.append({ + 'pid': proc.info['pid'], + 'name': proc.info['name'], + 'cpu': cpu_percent, + 'mem_mb': mem_info.rss / (1024 * 1024) if mem_info else 0, + 'num_threads': num_threads if num_threads is not None else 0 + }) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + except Exception as e: + system_utils.log_event(f"Error inesperado en get_top_processes: {e}") + continue + + processes_list.sort(key=lambda x: x['cpu'], reverse=True) + return processes_list[:limit] + +def iniciar_monitor_sistema(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, treeview_processes, root): + """Función que inicia el hilo de recolección de métricas.""" + monitor_thread = threading.Thread( + target=actualizar_metricas, + args=(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, treeview_processes, root) + ) + monitor_thread.daemon = True + monitor_thread.start() + +def actualizar_metricas(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, treeview_processes, root): + """Bucle principal del monitor: recolecta datos y actualiza los gráficos/tablas.""" + + # Inicialización de contadores de E/S de disco y red + net_io = psutil.net_io_counters() + last_bytes_sent = net_io.bytes_sent + last_bytes_recv = net_io.bytes_recv + + disk_io = psutil.disk_io_counters() + last_read_bytes = disk_io.read_bytes + last_write_bytes = disk_io.write_bytes + + psutil.cpu_percent(interval=None) + + while config.monitor_running: + try: + # 1. Recolección de datos + cpu_usage = psutil.cpu_percent(interval=None) + mem_details = psutil.virtual_memory() + mem_usage = mem_details.percent + core_usages = psutil.cpu_percent(interval=None, percpu=True) + for i, usage in enumerate(config.datos_cores): + config.datos_cores[i] = core_usages[i] + + current_net_io = psutil.net_io_counters() + speed_sent = (current_net_io.bytes_sent - last_bytes_sent) + speed_recv = (current_net_io.bytes_recv - last_bytes_recv) + last_bytes_sent = current_net_io.bytes_sent + last_bytes_recv = current_net_io.bytes_recv + + current_disk_io = psutil.disk_io_counters() + speed_read = (current_disk_io.read_bytes - last_read_bytes) + speed_write = (current_disk_io.write_bytes - last_write_bytes) + last_read_bytes = current_disk_io.read_bytes + last_write_bytes = current_disk_io.write_bytes + + top_processes = get_top_processes(limit=10) + + # Detección de Procesos Zombis + zombie_count = sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == psutil.STATUS_ZOMBIE) + + # 2. Actualizar datos de gráficos + config.datos_cpu.pop(0); config.datos_cpu.append(cpu_usage) + config.datos_mem.pop(0); config.datos_mem.append(mem_usage) + config.datos_net_sent.pop(0); config.datos_net_sent.append(speed_sent / 1024) + config.datos_net_recv.pop(0); config.datos_net_recv.append(speed_recv / 1024) + config.datos_disk_read.pop(0); config.datos_disk_read.append(speed_read / (1024 * 1024)) + config.datos_disk_write.pop(0); config.datos_disk_write.append(speed_write / (1024 * 1024)) + + # --- Chequeo de existencia de ventana ANTES de llamadas Tkinter --- + if not root.winfo_exists(): + break + + # Detección de Zombis (actualización de log si root existe) + if zombie_count > 0: + root.after(0, system_utils.log_event, f"ALERTA: Se detectaron {zombie_count} procesos ZOMBI.") + + # 3. Lógica de registro CSV + if config.registro_csv_activo: + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + data_row = [timestamp, config.datos_cpu[-1], config.datos_mem[-1], config.datos_net_sent[-1], config.datos_net_recv[-1]] + try: + with open(config.archivo_registro_csv, mode='a', newline='') as file: + writer = csv.writer(file) + writer.writerow(data_row) + except Exception as e: + root.after(0, system_utils.log_event, f"ERROR al escribir en CSV: {e}") + config.registro_csv_activo = False + root.after(0, config.label_2.config, {"text": "Registro: ERROR", "bg": "red"}) + + + # 4. Actualizar Gráficos y Treeview (en el hilo principal) + + # Chequeo de existencia de los widgets que reciben la actualización + if canvas.get_tk_widget().winfo_exists() and treeview_processes.winfo_exists(): + root.after(0, lambda: dibujar_graficos(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, mem_details, root)) + root.after(0, lambda: actualizar_process_treeview(treeview_processes, top_processes)) + + except Exception as e: + if root.winfo_exists(): + root.after(0, system_utils.log_event, f"Error en el hilo de monitor: {e}") + + time.sleep(1) + +def dibujar_graficos(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, mem_details, root): + """Dibuja y actualiza los 6 subplots. (Corrección: Uso de subplots_adjust)""" + plt.style.use('ggplot') + + try: + # --- GRÁFICO 1: CPU Total (Línea) --- + ax_cpu.clear() + ax_cpu.plot(config.tiempos, config.datos_cpu, color='red', linewidth=2) + ax_cpu.set_ylim(0, 100) + ax_cpu.set_title(f"CPU Total: {config.datos_cpu[-1]:.1f}%", fontsize=9) + ax_cpu.set_ylabel("Uso (%)", fontsize=7) + ax_cpu.tick_params(axis='both', which='major', labelsize=6) + ax_cpu.grid(True, linestyle='--', alpha=0.6) + + # --- GRÁFICO 2: MEMORIA RAM (Línea) --- + ax_mem.clear() + ax_mem.plot(config.tiempos, config.datos_mem, color='blue', linewidth=2) + ax_mem.set_ylim(0, 100) + ax_mem.set_title(f"RAM Total: {config.datos_mem[-1]:.1f}%", fontsize=9) + ax_mem.set_ylabel("Uso (%)", fontsize=7) + ax_mem.tick_params(axis='both', which='major', labelsize=6) + ax_mem.grid(True, linestyle='--', alpha=0.6) + + # --- GRÁFICO 3: CPU por Núcleo (Barra) --- + ax_cores.clear() + core_labels = [f"N{i}" for i in range(config.num_cores)] + ax_cores.bar(core_labels, config.datos_cores, color='darkred') + ax_cores.set_ylim(0, 100) + ax_cores.set_title("Uso por Núcleo", fontsize=9) + ax_cores.tick_params(axis='both', which='major', labelsize=6) + ax_cores.grid(axis='y', linestyle='--', alpha=0.6) + + # --- GRÁFICO 4: Red (Línea) --- + ax_net.clear() + ax_net.plot(config.tiempos, config.datos_net_sent, color='green', label='Enviado', linewidth=1.5) + ax_net.plot(config.tiempos, config.datos_net_recv, color='orange', label='Recibido', linewidth=1.5) + ax_net.set_title(f"Tráfico de Red (KB/s)", fontsize=9) + ax_net.set_xlabel("Tiempo (s)", fontsize=7) + ax_net.set_ylabel("KB/s", fontsize=7) + ax_net.tick_params(axis='both', which='major', labelsize=6) + ax_net.legend(loc='upper right', fontsize=6) + ax_net.grid(True, linestyle='--', alpha=0.6) + + # --- GRÁFICO 5: Distribución de Memoria (Tarta) --- + ax_pie.clear() + + total_mem = mem_details.total + used_mem = mem_details.used + free_mem = mem_details.free + + sizes = [used_mem, free_mem] + labels = [f'Usada ({sizes[0]/1024/1024:.0f}MB)', f'Libre ({sizes[1]/1024/1024:.0f}MB)'] + colors = ['#ff9999','#66b3ff'] + + ax_pie.pie(sizes, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, textprops={'fontsize': 7}) + ax_pie.set_title(f"Memoria Total: {system_utils.bytes_a_human_readable(total_mem)}", fontsize=9) + ax_pie.axis('equal') + + # --- GRÁFICO 6: Disk I/O (NUEVO) --- + ax_disk_io.clear() + ax_disk_io.plot(config.tiempos, config.datos_disk_read, color='purple', label='Lectura', linewidth=1.5) + ax_disk_io.plot(config.tiempos, config.datos_disk_write, color='brown', label='Escritura', linewidth=1.5) + + max_io = max(max(config.datos_disk_read), max(config.datos_disk_write)) * 1.1 or 1 + + ax_disk_io.set_ylim(0, max_io) + ax_disk_io.set_title(f"Disco I/O (MB/s)", fontsize=9) + ax_disk_io.set_xlabel("Tiempo (s)", fontsize=7) + ax_disk_io.set_ylabel("MB/s", fontsize=7) + ax_disk_io.tick_params(axis='both', which='major', labelsize=6) + ax_disk_io.legend(loc='upper right', fontsize=6) + ax_disk_io.grid(True, linestyle='--', alpha=0.6) + + # CORRECCIÓN DE DIBUJO (Ajuste manual) + plt.subplots_adjust( + left=0.07, right=0.98, + bottom=0.08, top=0.95, + wspace=0.3, + hspace=0.4 + ) + + canvas.draw() + + except Exception as e: + system_utils.log_event(f"ERROR CRÍTICO DE DIBUJO: Matplotlib falló con {e}. (Gráficos congelados)") + + +def actualizar_process_treeview(tree, processes_data): + """Limpia y rellena el Treeview con los datos de los procesos.""" + for item in tree.get_children(): + tree.delete(item) + + for p in processes_data: + tree.insert('', tk.END, values=( + p['pid'], + f"{p['cpu']:.1f}%", + f"{p['mem_mb']:.1f}MB", + p['num_threads'], + p['name'] + )) + +def terminar_proceso(treeview_processes): + """Intenta terminar el proceso seleccionado en el Treeview.""" + selected_item = treeview_processes.focus() + if not selected_item: + messagebox.showwarning("Advertencia", "Selecciona un proceso para terminar.") + return + + values = treeview_processes.item(selected_item, 'values') + pid_to_kill = int(values[0]) + name_to_kill = values[-1] + + if not messagebox.askyesno( + "Confirmación", + f"¿Estás seguro de que quieres terminar el proceso {name_to_kill} (PID: {pid_to_kill})?" + ): + return + + def kill_thread(pid, name): + """Función que ejecuta el kill en un hilo y registra el resultado.""" + try: + proc = psutil.Process(pid) + proc.terminate() + system_utils.log_event(f"Proceso {name} (PID: {pid}) terminado exitosamente.") + except psutil.NoSuchProcess: + system_utils.log_event(f"ERROR: Proceso {name} (PID: {pid}) no encontrado.") + except psutil.AccessDenied: + system_utils.log_event(f"ERROR: No se pudo terminar el proceso {name}. Permiso denegado.") + except Exception as e: + system_utils.log_event(f"ERROR al terminar {name}: {e}") + + threading.Thread(target=kill_thread, args=(pid_to_kill, name_to_kill)).start() \ No newline at end of file diff --git a/proyecto.py b/proyecto.py new file mode 100644 index 0000000..e9fa6fb --- /dev/null +++ b/proyecto.py @@ -0,0 +1,19 @@ +# proyecto.py +import tkinter as tk +import os +import sys + +# Importación directa de los módulos +import ui_layout + +# =============================================== +# Ejecución de la aplicación +# =============================================== +if __name__ == "__main__": + root = tk.Tk() + root.title("Monitor de Sistema - TFG") + root.geometry("1100x850") + + ui_layout.crear_ui_completa(root) + + root.mainloop() \ No newline at end of file diff --git a/system_utils.py b/system_utils.py new file mode 100644 index 0000000..0bf658d --- /dev/null +++ b/system_utils.py @@ -0,0 +1,963 @@ +# system_utils.py +import tkinter as tk +from tkinter import messagebox, simpledialog, filedialog, ttk +import datetime +import webbrowser +import subprocess +import csv +import threading +import time +import psutil +import os +import platform +import uuid +import pygame +import json +import random + +# Importaciones directas de módulos (Acceso con el prefijo del módulo) +import config +import monitor_manager + +# Inicializar pygame mixer +try: + pygame.mixer.init() +except Exception as e: + print(f"ADVERTENCIA: No se pudo iniciar pygame.mixer: {e}") + +# --- Constante para la comunicación de progreso --- +PROGRESS_FILE = os.path.join(config.BASE_DIR, "task_progress.txt") + +# =============================================== +# Log y Soporte +# =============================================== +def log_event(message): + """Escribe un mensaje en el log del sistema.""" + if config.system_log and config.system_log.winfo_exists(): + timestamp = datetime.datetime.now().strftime("[%H:%M:%S] ") + config.system_log.config(state=tk.NORMAL) + config.system_log.insert(tk.END, timestamp + message + "\n") + config.system_log.see(tk.END) + config.system_log.config(state=tk.DISABLED) + +def bytes_a_human_readable(n): + """Convierte bytes a KB, MB, GB, etc.""" + symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + prefix = {} + for i, s in enumerate(symbols): + prefix[s] = 1 << (i + 1) * 10 + + for s in reversed(symbols): + if n >= prefix[s]: + value = float(n) / prefix[s] + return f'{value:.2f} {s}iB' + return f'{n} B' + +# =============================================== +# Lógica de Persistencia de Alarmas +# =============================================== + +def guardar_alarmas(): + """Guarda la lista de alarmas en un archivo JSON.""" + + # 1. Preparar datos para serializar: Convertir objetos datetime a strings ISO + data_to_save = {} + + # Encontramos el último ID usado + last_id = 0 + + for uid, data in config.alarmas_programadas.items(): + data_to_save[uid] = { + 'time_str': data['time'].isoformat(), # Convertir datetime a string + 'active': data['active'], + 'message': data['message'], + 'sound_file': data['sound_file'] + } + last_id = max(last_id, uid) + + final_data = { + "last_id": last_id, + "alarms": data_to_save + } + + try: + os.makedirs(os.path.dirname(config.ALARM_SAVE_FILE), exist_ok=True) + with open(config.ALARM_SAVE_FILE, 'w', encoding='utf-8') as f: + json.dump(final_data, f, indent=4) + log_event("Datos de alarma guardados con éxito.") + except Exception as e: + log_event(f"ERROR: No se pudieron guardar las alarmas: {e}") + +def cargar_alarmas(treeview_alarmas=None, root=None): + """Carga las alarmas desde el archivo JSON y las añade al modelo y al Treeview.""" + + if not os.path.exists(config.ALARM_SAVE_FILE): + return + + try: + with open(config.ALARM_SAVE_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + + config.alarma_counter = data.get("last_id", 0) + alarm_data = data.get("alarms", {}) + + for uid_str, data in alarm_data.items(): + uid = int(uid_str) + + # 1. Convertir string ISO de vuelta a objeto datetime + target_time = datetime.datetime.fromisoformat(data['time_str']) + + # 2. Re-agregar al modelo + config.alarmas_programadas[uid] = { + 'time': target_time, + 'active': data['active'], + 'message': data['message'], + 'sound_file': data['sound_file'] + } + + # 3. Si se proporciona Treeview, actualizar la interfaz + if treeview_alarmas: + status = "ACTIVA" if data['active'] else "INACTIVA" + treeview_alarmas.insert('', tk.END, values=( + uid, + target_time.strftime('%H:%M'), + data['message'], + status, + target_time.strftime('%Y-%m-%d') + ), tags=('active',) if data['active'] else ('inactive',)) + + # Si la alarma está activa, aseguramos que el hilo de verificación inicie + if data['active'] and root and not hasattr(agregar_alarma, 'hilo_activo'): + threading.Thread(target=lambda: verificar_alarma(root, treeview_alarmas), daemon=True).start() + setattr(agregar_alarma, 'hilo_activo', True) + + log_event(f"Se cargaron {len(alarm_data)} alarmas.") + + except Exception as e: + log_event(f"ERROR: Fallo al cargar el archivo de alarmas: {e}") + config.alarmas_programadas = {} # Limpiar modelo en caso de fallo + +# =============================================== +# Funcionalidades de Alarma (Modificadas para Pygame) +# =============================================== + +def verificar_alarma(root, treeview_alarmas): + """ + Función del hilo secundario que verifica la hora actual contra la hora objetivo. + """ + while config.monitor_running: + + alarmas_a_chequear = list(config.alarmas_programadas.items()) + now = datetime.datetime.now() + + for alarma_id, data in alarmas_a_chequear: + if data['active']: + target = data['time'] + + # Comparamos hora y minuto + if now.hour == target.hour and now.minute == target.minute and now.second < 2: + # Alarma alcanzada! + root.after(0, lambda: notificar_alarma_alcanzada(alarma_id, data, treeview_alarmas)) + + # Desactivamos la alarma en el modelo para que no se repita + data['active'] = False + guardar_alarmas() # Guardar después de desactivar + + time.sleep(1) # Chequeamos cada segundo + +def notificar_alarma_alcanzada(alarma_id, data, treeview_alarmas): + """Muestra la notificación visual, reproduce el sonido, actualiza el Treeview y registra el evento.""" + + # Detenemos música antes de empezar a sonar la alarma + detener_mp3() + + # 1. Reproducir Sonido (Usando pygame) + sound_file = data['sound_file'] # Usamos el archivo almacenado en el modelo + + if sound_file and os.path.exists(sound_file): + try: + if pygame.mixer.music.get_busy(): + pygame.mixer.music.stop() + + pygame.mixer.music.load(sound_file) + pygame.mixer.music.set_volume(config.alarma_volumen) + pygame.mixer.music.play(-1) # Reproducir en bucle (-1) + config.alarma_sonando = True + log_event(f"Sonido de alarma '{os.path.basename(sound_file)}' iniciado.") + except Exception as e: + log_event(f"ERROR al reproducir sonido con pygame: {e}") + else: + log_event("ADVERTENCIA: Archivo de sonido no encontrado o no válido.") + + # 2. Notificación visual + msg = f"¡Alarma programada alcanzada!\nTarea: {data['message']}" + log_event(f"ALARMA ACTIVADA: '{data['message']}' a las {data['time'].strftime('%H:%M:%S')}") + messagebox.showinfo("ALARMA", msg) + + # 3. Actualizar el Treeview para mostrar 'INACTIVA' + for item in treeview_alarmas.get_children(): + values = treeview_alarmas.item(item, 'values') + if values and values[0] == str(alarma_id): + treeview_alarmas.item(item, values=(alarma_id, values[1], values[2], "INACTIVA", values[4]), tags=('inactive',)) + break + + guardar_alarmas() # Guardar después de actualizar la interfaz + +def detener_sonido_alarma(): + """Detiene la reproducción de sonido de la alarma usando pygame.""" + if config.alarma_sonando and pygame.mixer.music.get_busy(): + pygame.mixer.music.stop() + config.alarma_sonando = False + log_event("Reproducción de alarma detenida.") + elif config.alarma_sonando: + config.alarma_sonando = False + log_event("Estado de alarma limpiado. No estaba sonando.") + +def ajustar_volumen_alarma(nuevo_volumen_str): + """Ajusta el volumen de la alarma (rango 0.0 a 1.0) desde el slider (0-100).""" + try: + vol_int = int(float(nuevo_volumen_str)) + vol = vol_int / 100.0 # Convertir de 0-100 a 0.0-1.0 + + # 1. Actualizar la variable de configuración para futuras alarmas/música + config.alarma_volumen = vol + + # 2. Si hay algo sonando, ajustar inmediatamente el volumen + if pygame.mixer.music.get_busy(): + pygame.mixer.music.set_volume(vol) + + log_event(f"Volumen de reproducción ajustado a {vol:.2f}.") + except Exception as e: + log_event(f"Error al ajustar volumen: {e}") + + +def agregar_alarma(root, hora_str, minuto_str, tarea, treeview_alarmas, window_to_close=None, sound_file=None): + """ + Añade una nueva alarma a la lista (usando ID autonumérico) y actualiza el Treeview. + Acepta el argumento 'sound_file' para la ruta del sonido seleccionado. + """ + + try: + hora = int(hora_str) + minuto = int(minuto_str) + tarea = tarea.strip() + + if not tarea: + tarea = "Alarma sin descripción" + + # Cálculo de la hora objetivo + now = datetime.datetime.now() + target_time = datetime.datetime(now.year, now.month, now.day, hora, minuto, 0) + + if target_time < now: + target_time += datetime.timedelta(days=1) + + # 1. Generar ID autonumérico y único + config.alarma_counter += 1 + alarma_id = config.alarma_counter + + # 2. Añadir al modelo + config.alarmas_programadas[alarma_id] = { + 'time': target_time, + 'active': True, + 'message': tarea, + 'sound_file': sound_file if sound_file else config.ALERTA_SOUND_FILE # Usar el seleccionado + } + + # 3. Añadir al Treeview + treeview_alarmas.insert('', tk.END, values=( + alarma_id, + target_time.strftime('%H:%M'), + tarea, + "ACTIVA", + target_time.strftime('%Y-%m-%d') + ), tags=('active',)) + + # Configuramos los tags visuales + treeview_alarmas.tag_configure('active', background='#FFCCCC') # Fondo rojo claro si está activa + treeview_alarmas.tag_configure('inactive', background='#DDDDDD') + + log_event(f"Nueva alarma ({alarma_id}) programada: {tarea}") + + # Guardar inmediatamente después de agregar + guardar_alarmas() + + # 4. Iniciar el hilo de verificación si no está activo + if not hasattr(agregar_alarma, 'hilo_activo'): + threading.Thread(target=lambda: verificar_alarma(root, treeview_alarmas), daemon=True).start() + setattr(agregar_alarma, 'hilo_activo', True) + + # 5. Cerrar la ventana flotante si existe + if window_to_close: + window_to_close.destroy() + + + except ValueError: + messagebox.showerror("Error de entrada", "Asegúrate de seleccionar una hora y minuto válidos (00-23, 00-59).") + +def toggle_alarma(treeview_alarmas): + """Activa o desactiva la alarma seleccionada.""" + selected_item = treeview_alarmas.focus() + if not selected_item: + messagebox.showwarning("Advertencia", "Selecciona una alarma de la lista.") + return + + alarma_id = int(treeview_alarmas.item(selected_item, 'values')[0]) + data = config.alarmas_programadas.get(alarma_id) + + if data: + data['active'] = not data['active'] + new_status = "ACTIVA" if data['active'] else "INACTIVA" + + # Actualizar Treeview y tags visuales + treeview_alarmas.item(selected_item, values=(alarma_id, data['time'].strftime('%H:%M'), data['message'], new_status, data['time'].strftime('%Y-%m-%d')), tags=('active',) if data['active'] else ('inactive',)) + + log_event(f"Alarma {alarma_id} toggled a {new_status}.") + guardar_alarmas() # Guardar después del toggle + +def eliminar_alarma(treeview_alarmas): + """Elimina la alarma seleccionada del modelo y del Treeview.""" + selected_item = treeview_alarmas.focus() + if not selected_item: + messagebox.showwarning("Advertencia", "Selecciona una alarma para eliminar.") + return + + alarma_id = int(treeview_alarmas.item(selected_item, 'values')[0]) + + if messagebox.askyesno("Confirmar Eliminación", f"¿Estás seguro de que deseas eliminar la alarma con ID {alarma_id}?"): + + # 1. Eliminar del modelo + if alarma_id in config.alarmas_programadas: + del config.alarmas_programadas[alarma_id] + + # 2. Eliminar del Treeview + treeview_alarmas.delete(selected_item) + log_event(f"Alarma {alarma_id} eliminada.") + guardar_alarmas() # Guardar después de la eliminación + +def modificar_alarma_existente(root, alarma_id, hora_str, minuto_str, tarea, treeview_alarmas, window_to_close, sound_file): + """ + Sobreescribe los datos de una alarma existente en el modelo y actualiza el Treeview. + """ + + try: + data = config.alarmas_programadas.get(alarma_id) + if not data: + messagebox.showerror("Error", "Alarma no encontrada en el sistema.") + return + + hora = int(hora_str) + minuto = int(minuto_str) + tarea = tarea.strip() or "Alarma sin descripción" + + # 1. Recalcular la hora objetivo + now = datetime.datetime.now() + target_time = datetime.datetime(now.year, now.month, now.day, hora, minuto, 0) + if target_time < now: + target_time += datetime.timedelta(days=1) + + # 2. Actualizar el modelo (manteniendo el estado 'active' actual) + data['time'] = target_time + data['message'] = tarea + data['sound_file'] = sound_file + + # 3. Actualizar el Treeview + new_status = "ACTIVA" if data['active'] else "INACTIVA" + + # Encontramos el item en el Treeview para actualizarlo + for item in treeview_alarmas.get_children(): + if treeview_alarmas.item(item, 'values')[0] == str(alarma_id): + treeview_alarmas.item(item, values=( + alarma_id, + target_time.strftime('%H:%M'), + tarea, + new_status, + target_time.strftime('%Y-%m-%d') + ), tags=('active',) if data['active'] else ('inactive',)) + break + + log_event(f"Alarma {alarma_id} modificada y re-programada para {target_time.strftime('%H:%M')}.") + guardar_alarmas() + window_to_close.destroy() + + except ValueError: + messagebox.showerror("Error de entrada", "Asegúrate de seleccionar una hora y minuto válidos.") + except Exception as e: + log_event(f"ERROR al modificar alarma: {e}") + messagebox.showerror("Error", f"Error al modificar la alarma: {e}") + + +def seleccionar_archivo_alarma(root, label_archivo): + """Abre un diálogo para seleccionar un archivo de sonido de la carpeta alarmas.""" + + try: + # Asegurar que la carpeta exista antes de usarla como directorio inicial + os.makedirs(config.ALARM_FOLDER, exist_ok=True) + except Exception as e: + log_event(f"ERROR: No se pudo crear la carpeta de alarmas: {e}") + messagebox.showerror("Error", f"No se pudo crear el directorio de alarmas: {e}") + + try: + archivo_seleccionado = filedialog.askopenfilename( + initialdir=config.ALARM_FOLDER, + title="Seleccionar Sonido de Alarma", + filetypes=(("Archivos de Audio", "*.wav *.mp3"), ("Todos los archivos", "*.*")), + parent=root + ) + + if archivo_seleccionado: + # 1. Almacenar la ruta completa en la variable de configuración global + config.ALERTA_SOUND_FILE = archivo_seleccionado + + # 2. Actualizar el Label en la UI para mostrar el nombre del archivo + label_archivo.config(text=os.path.basename(archivo_seleccionado)) + + log_event(f"Sonido seleccionado: {os.path.basename(archivo_seleccionado)}") + return archivo_seleccionado + else: + log_event("Selección de sonido cancelada.") + return config.ALERTA_SOUND_FILE + + except Exception as e: + log_event(f"ERROR al iniciar el diálogo de selección de archivo: {e}") + messagebox.showerror("Error", "No se pudo iniciar el diálogo de selección de archivo.") + return config.ALERTA_SOUND_FILE + + +# =============================================== +# Funcionalidades de Reproducción de Música (NUEVAS) +# =============================================== + +def seleccionar_mp3(root, label_archivo): + """Abre un diálogo para seleccionar un archivo MP3 o WAV.""" + + try: + # Directorio inicial + initial_dir = os.path.join(config.BASE_DIR, "data") + os.makedirs(initial_dir, exist_ok=True) + + archivo_seleccionado = filedialog.askopenfilename( + initialdir=initial_dir, + title="Seleccionar Archivo de Música", + filetypes=(("Archivos de Audio", "*.mp3 *.wav"), ("Todos los archivos", "*.*")), + parent=root + ) + + if archivo_seleccionado: + config.current_music_file = archivo_seleccionado + label_archivo.config(text=os.path.basename(archivo_seleccionado)) + log_event(f"Música seleccionada: {os.path.basename(archivo_seleccionado)}") + return archivo_seleccionado + else: + log_event("Selección de música cancelada.") + return None + + except Exception as e: + log_event(f"ERROR al iniciar el diálogo de selección de música: {e}") + messagebox.showerror("Error", "No se pudo iniciar el diálogo de selección de archivo.") + return None + +def reproducir_mp3(root): + """Carga y reproduce el archivo seleccionado, asegurando que no haya conflictos con la alarma.""" + + if not config.current_music_file or not os.path.exists(config.current_music_file): + messagebox.showwarning("Advertencia", "Por favor, selecciona un archivo de música primero.") + return + + # Detener cualquier reproducción previa (alarma o música) + detener_sonido_alarma() # Detiene la alarma si está sonando + detener_mp3() # Detiene la música si está sonando + + try: + pygame.mixer.music.load(config.current_music_file) + # Reutilizamos la variable de volumen global, ya que Pygame Mixer tiene un solo canal de control de volumen + pygame.mixer.music.set_volume(config.alarma_volumen) + pygame.mixer.music.play(-1) # Reproducir en bucle + config.music_sonando = True + log_event(f"Reproducción de música iniciada: {os.path.basename(config.current_music_file)}") + except pygame.error as e: + log_event(f"ERROR de Pygame al reproducir música: {e}") + messagebox.showerror("Error de Reproducción", f"No se pudo reproducir el archivo. Asegúrate de que sea compatible con Pygame. ({e})") + except Exception as e: + log_event(f"ERROR inesperado al reproducir música: {e}") + +def detener_mp3(): + """Detiene la reproducción de música si está activa.""" + if hasattr(config, 'music_sonando') and config.music_sonando and pygame.mixer.music.get_busy(): + pygame.mixer.music.stop() + config.music_sonando = False + log_event("Reproducción de música detenida.") + elif hasattr(config, 'music_sonando') and config.music_sonando: + config.music_sonando = False + log_event("Estado de música limpiado. No estaba sonando.") + +def ajustar_volumen_mp3(nuevo_volumen_str): + """Ajusta el volumen de Pygame (rango 0.0 a 1.0) desde el slider (0-100).""" + # Reutiliza la misma lógica que ajustar_volumen_alarma, ya que Pygame Mixer solo tiene un volumen maestro. + ajustar_volumen_alarma(nuevo_volumen_str) + + +# =============================================== +# Funcionalidades Externas (Existentes) +# =============================================== +def lanzar_url(url): + """Abre una URL en el navegador y registra el evento.""" + try: + webbrowser.open_new_tab(url) + log_event(f"Lanzando URL: {url}") + except Exception as e: + log_event(f"Error al intentar abrir la URL {url}: {e}") + +def ejecutar_script_en_hilo(status_label, root): + """Ejecuta un script de Bash en un hilo separado con ProgressBar.""" + + def run_script(): + if not root.winfo_exists(): return + + # 1. PREPARACIÓN E INICIO DE PROGRESSBAR + if os.path.exists(config.PROGRESS_FILE): os.remove(config.PROGRESS_FILE) + if config.progress_bar: + root.after(0, config.progress_bar.config, {"value": 0, "maximum": 100}) + root.after(0, config.progress_bar.start, 20) + + status_label.after(0, status_label.config, {"text": "Ejecutando Backup...", "bg": "orange"}) + log_event("Iniciando copia de seguridad (backup)...") + + try: + command = ["bash", config.SCRIPT_PATH, config.BASE_DIR] + result = subprocess.run(command, capture_output=True, text=True, check=False) + + if not root.winfo_exists(): return # Doble chequeo después de subprocess + + # 2. FINALIZACIÓN Y LIMPIEZA + if config.progress_bar: + root.after(0, config.progress_bar.stop) + root.after(0, config.progress_bar.config, {"value": 100 if result.returncode == 0 else 0}) + + if result.returncode == 0: + if root.winfo_exists(): root.after(0, status_label.config, {"text": "Backup: OK", "bg": "green"}) + log_event("Copia de seguridad finalizada con éxito.") + else: + if root.winfo_exists(): root.after(0, status_label.config, {"text": f"Backup: ERROR ({result.returncode})", "bg": "red"}) + log_event(f"ERROR en la copia de seguridad. Código: {result.returncode}. Verifique la terminal.") + + except Exception as e: + if config.progress_bar: root.after(0, config.progress_bar.stop) + if root.winfo_exists(): root.after(0, status_label.config, {"text": f"Error desconocido: {str(e)}", "bg": "red"}) + log_event(f"Error desconocido en backup: {str(e)}") + + finally: + if os.path.exists(config.PROGRESS_FILE): os.remove(config.PROGRESS_FILE) + + + # 3. INICIO DEL HILO DE TAREA Y EL HILO MONITOR + threading.Thread(target=run_script, daemon=True).start() + threading.Thread(target=lambda: read_progress_pipe(root), daemon=True).start() + + +def read_progress_pipe(root): + """Hilo que monitorea un archivo temporal para actualizar la ProgressBar.""" + if not config.progress_bar: return + + while config.monitor_running: + if not root.winfo_exists(): break + + try: + if os.path.exists(config.PROGRESS_FILE): + with open(config.PROGRESS_FILE, 'r') as f: + content = f.read().strip() + + if content.isdigit(): + value = int(content) + root.after(0, config.progress_bar.config, {"value": value}) + root.after(0, config.progress_bar.update) + elif content.startswith("STATUS:"): + log_event(content[7:]) + + except Exception as e: + pass + + time.sleep(0.1) + + +def update_time(status_bar, root): + """Función que actualiza la hora y el día de la semana en un label (Hilo).""" + while config.monitor_running: + now = datetime.datetime.now() + day_of_week = now.strftime("%A") + time_str = now.strftime("%H:%M:%S") + date_str = now.strftime("%Y-%m-%d") + label_text = f"{day_of_week}, {date_str} - {time_str}" + + if root.winfo_exists(): + root.after(1000, lambda: status_bar.winfo_exists() and status_bar.config(text=label_text)) + else: + break + + time.sleep(1) + +def manejar_registro_csv(status_label): + """Inicia o detiene la escritura de datos de recursos a un archivo CSV.""" + + if config.registro_csv_activo: + config.registro_csv_activo = False + status_label.config(text="Registro: Detenido", bg="gray") + log_event("Registro de historial de recursos detenido.") + else: + config.registro_csv_activo = True + status_label.config(text="Registro: ACTIVO", bg="gold") + log_event(f"Registro de historial de recursos iniciado en: {config.archivo_registro_csv}") + + try: + with open(config.archivo_registro_csv, mode='a', newline='') as file: + writer = csv.writer(file) + if file.tell() == 0: + writer.writerow(['Timestamp', 'CPU_Total (%)', 'RAM_Total (%)', 'Net_Sent (KB/s)', 'Net_Recv (KB/s)']) + except Exception as e: + log_event(f"ERROR: No se pudo iniciar el registro CSV: {e}") + config.registro_csv_activo = False + status_label.config(text="Registro: ERROR", bg="red") + + return config.registro_csv_activo + +# =============================================== +# Funciones del Editor de Texto (Existentes) +# =============================================== + +def nuevo_archivo(): + """Limpia el contenido actual del editor de texto.""" + if not config.editor_texto: + log_event("ERROR: Editor de texto no inicializado.") + return + + config.editor_texto.delete("1.0", tk.END) + log_event("Editor de texto limpiado. Nuevo archivo iniciado.") + +def abrir_carpeta_especifica(ruta, nombre): + """Abre una carpeta específica del proyecto en el explorador de archivos.""" + + try: + os.makedirs(ruta, exist_ok=True) + # Usamos subprocess.Popen para ser compatible con más sistemas + if platform.system() == "Windows": + subprocess.Popen(['explorer', ruta]) + elif platform.system() == "Darwin": # macOS + subprocess.Popen(['open', ruta]) + else: # Asume Linux/Unix (usa xdg-open) + subprocess.Popen(['xdg-open', ruta]) + + log_event(f"Carpeta '{nombre}' abierta: {ruta}") + except Exception as e: + log_event(f"ERROR al abrir la carpeta '{nombre}': {e}") + messagebox.showerror("Error", f"No se pudo abrir el directorio {nombre}: {e}") + +def abrir_carpeta_notas(): + """Función wrapper para abrir la carpeta de notas.""" + abrir_carpeta_especifica(config.NOTES_FOLDER, "Notas") + + +def abrir_archivo(root): + """Muestra un diálogo para seleccionar y abrir un archivo .txt de la carpeta de notas.""" + if not config.editor_texto: + log_event("ERROR: Editor de texto no inicializado.") + return + + # Usar la nueva carpeta de notas + ruta_notas = config.NOTES_FOLDER + + # Asegurar que la carpeta exista antes de abrir el diálogo + os.makedirs(ruta_notas, exist_ok=True) + + archivo_seleccionado = filedialog.askopenfilename( + initialdir=ruta_notas, + title="Abrir Archivo de Notas", + filetypes=(("Archivos de texto", "*.txt"), ("Todos los archivos", "*.*")), + parent=root + ) + + if not archivo_seleccionado: + log_event("Apertura de archivo de notas cancelada.") + return + + try: + with open(archivo_seleccionado, 'r', encoding='utf-8') as f: + contenido = f.read() + + config.editor_texto.delete("1.0", tk.END) + config.editor_texto.insert("1.0", contenido) + + nombre = os.path.basename(archivo_seleccionado) + log_event(f"Archivo de notas cargado con éxito: {nombre}") + messagebox.showinfo("Abierto", f"Archivo '{nombre}' cargado con éxito.") + + except Exception as e: + log_event(f"ERROR al abrir o leer el archivo de notas: {e}") + messagebox.showerror("Error de Lectura", f"No se pudo leer el archivo seleccionado: {e}") + + +def guardar_texto(root): + """ + Pide al usuario un nombre de archivo y guarda el contenido del editor + en la carpeta Proyecto/data/notas/ como un archivo .txt. + """ + if not config.editor_texto: + log_event("ERROR: Editor de texto no inicializado.") + return + + try: + nombre_archivo = simpledialog.askstring( + "Guardar Archivo", + "Introduce el nombre del archivo (ej: notas)", + parent=root + ) + except Exception as e: + log_event(f"ERROR al iniciar diálogo de guardado: {e}") + return + + if not nombre_archivo: + log_event("Guardado cancelado por el usuario.") + return + + ruta_notas = config.NOTES_FOLDER # Usar la nueva carpeta de notas + + if not nombre_archivo.lower().endswith('.txt'): + nombre_archivo += '.txt' + + ruta_completa = os.path.join(ruta_notas, nombre_archivo) + + contenido = config.editor_texto.get("1.0", tk.END) + + try: + os.makedirs(ruta_notas, exist_ok=True) + + with open(ruta_completa, 'w', encoding='utf-8') as f: + f.write(contenido) + + log_event(f"Archivo de notas guardado con éxito: {ruta_completa}") + messagebox.showinfo("Guardado", f"Archivo guardado como:\n{nombre_archivo}") + + except PermissionError: + error_msg = f"ERROR: Permiso denegado al escribir en {ruta_completa}." + log_event(error_msg) + messagebox.showerror("Error de Permiso", error_msg) + except Exception as e: + error_msg = f"ERROR al guardar {nombre_archivo}: {e}" + log_event(error_msg) + messagebox.showerror("Error de Escritura", error_msg) + +# =============================================== +# Funciones de Web Scraping Adicionales +# =============================================== + +def detener_scraping(): + """Detiene la ejecución del hilo de scraping si está activo.""" + if config.scraping_running: + config.scraping_running = False + if config.scraping_progress_bar: + config.scraping_progress_bar.stop() + log_event("🛑 Proceso de Web Scrapear solicitado para detenerse.") + + # Opcional: Escribir un mensaje de detención en el área de salida + if config.scraping_output_text: + config.scraping_output_text.insert(tk.END, "\n--- PROCESO CANCELADO POR EL USUARIO ---\n") + + else: + log_event("El proceso de Web Scrapear no está actualmente activo.") + +def guardar_scraping(contenido, root): + """ + Guarda el contenido del ScrolledText de scraping en un archivo TXT + en la carpeta Proyecto/data/scraping/. + """ + if not contenido or contenido.strip() == "Resultado de la Extracción:": + messagebox.showwarning("Advertencia", "El área de resultados está vacía.") + return + + # Pedir un nombre de archivo + try: + nombre_base = datetime.datetime.now().strftime("scraping_%Y%m%d_%H%M%S") + nombre_archivo = simpledialog.askstring( + "Guardar Resultado", + f"Introduce el nombre del archivo (predeterminado: {nombre_base})", + initialvalue=nombre_base, + parent=root + ) + except Exception as e: + log_event(f"ERROR al iniciar diálogo de guardado de scraping: {e}") + return + + if not nombre_archivo: + log_event("Guardado de scraping cancelado por el usuario.") + return + + # Rutas + ruta_scrapping = config.SCRAPING_FOLDER + + if not nombre_archivo.lower().endswith('.txt'): + nombre_archivo += '.txt' + + ruta_completa = os.path.join(ruta_scrapping, nombre_archivo) + + try: + # Asegurar que la carpeta exista + os.makedirs(ruta_scrapping, exist_ok=True) + + with open(ruta_completa, 'w', encoding='utf-8') as f: + f.write(contenido) + + log_event(f"Resultado de scraping guardado con éxito: {ruta_completa}") + messagebox.showinfo("Guardado", f"Resultado de scraping guardado como:\n{nombre_archivo}") + + except Exception as e: + error_msg = f"ERROR al guardar el resultado de scraping: {e}" + log_event(error_msg) + messagebox.showerror("Error de Escritura", error_msg) + +def abrir_archivo_scraping_config(root): + """ + Abre un diálogo para seleccionar un archivo JSON de configuración de scraping + y lo carga en config.scraping_config_data. + """ + try: + # 1. Asegurar que la carpeta exista + os.makedirs(config.SCRAPING_CONFIG_FOLDER, exist_ok=True) + + # 2. Abrir diálogo de selección + archivo_seleccionado = filedialog.askopenfilename( + initialdir=config.SCRAPING_CONFIG_FOLDER, + title="Cargar Configuración de Scraping (JSON)", + filetypes=(("Archivos JSON", "*.json"), ("Todos los archivos", "*.*")), + parent=root + ) + + if not archivo_seleccionado: + log_event("Carga de configuración de scraping cancelada.") + return + + # 3. Cargar el JSON + with open(archivo_seleccionado, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 4. Validar estructura mínima (opcional, pero buena práctica) + required_keys = ['type', 'selector'] + if not all(key in data for key in required_keys): + messagebox.showerror("Error de Configuración", f"El archivo JSON debe contener al menos las claves: {', '.join(required_keys)}.") + log_event("ERROR: Configuración JSON incompleta.") + return + + # 5. Guardar en la configuración global + config.scraping_config_data = data + + # 6. Actualizar campos de la UI + file_name = os.path.basename(archivo_seleccionado) + + # Actualizar URL de la interfaz si el JSON tiene 'url' + if 'url' in data and config.scraping_url_input: + config.scraping_url_input.set(data['url']) + + # Actualizar Label del archivo cargado + if config.scraping_config_file_label: + config.scraping_config_file_label.config(text=f"Config: [{file_name}]") + + log_event(f"Configuración de scraping '{file_name}' cargada con éxito.") + messagebox.showinfo("Éxito", f"Configuración de '{file_name}' cargada. Presiona 'Iniciar Scrapear'.") + + except FileNotFoundError: + log_event("ERROR: Archivo de configuración no encontrado.") + messagebox.showerror("Error", "Archivo no encontrado.") + except json.JSONDecodeError: + log_event("ERROR: El archivo no es un JSON válido.") + messagebox.showerror("Error", "El archivo cargado no es un formato JSON válido.") + except Exception as e: + log_event(f"ERROR al cargar la configuración de scraping: {e}") + messagebox.showerror("Error", f"Fallo al cargar la configuración: {e}") + +# =============================================== +# Funcionalidad de Juegos (NUEVO) +# =============================================== + +def simular_juego_camellos(root): + """ + Simula una carrera de camellos con actualizaciones Thread-Safe, + mostrando la simulación en una nueva ventana Toplevel. + """ + + # 1. Chequeo de juego activo + if hasattr(config, 'juego_window') and config.juego_window and config.juego_window.winfo_exists(): + messagebox.showwarning("Advertencia", "Ya hay un juego activo. Ciérralo para iniciar uno nuevo.") + return + + # 2. Inicializar la ventana de juego + juego_window = tk.Toplevel(root) + juego_window.title("Carrera de Camellos 🐫 (Thread-Safe)") + juego_window.geometry("550x300") + juego_window.resizable(False, False) + config.juego_window = juego_window + config.juego_running = True + + # Variables de estado del juego + posiciones = {'Camello A': 0, 'Camello B': 0, 'Camello C': 0} + meta = 35 + + # 3. Widgets de la UI + tk.Label(juego_window, text="¡Iniciando Carrera!", font=('Helvetica', 14, 'bold')).pack(pady=10) + + track_frame = tk.Frame(juego_window) + track_frame.pack(padx=20, pady=10, fill='x') + + # Labels para la pista + labels = {} + for i, camello in enumerate(posiciones.keys()): + tk.Label(track_frame, text=f"{camello}:", font=('Helvetica', 10, 'bold')).grid(row=i, column=0, sticky='w', padx=5, pady=5) + # Usamos un font monoespaciado para que la barra se vea bien + labels[camello] = tk.Label(track_frame, text="🐫", anchor='w', width=45, font=('Courier', 10), bg='lightgray', relief='sunken') + labels[camello].grid(row=i, column=1, sticky='ew', padx=5, pady=5) + + resultado_label = tk.Label(juego_window, text="En curso...", font=('Helvetica', 12)) + resultado_label.pack(pady=10) + + def cerrar_juego(): + """Función que maneja el cierre de la ventana del juego.""" + config.juego_running = False + juego_window.destroy() + log_event("Juego de camellos cerrado.") + + juego_window.protocol("WM_DELETE_WINDOW", cerrar_juego) + + def avanzar_carrera(): + """Lógica de avance de la carrera (simulación).""" + if not config.juego_running or not juego_window.winfo_exists(): + return + + ganador = None + + for camello in posiciones.keys(): + if posiciones[camello] < meta: + # Avance aleatorio (1-3 pasos) + avance = random.randint(1, 3) + posiciones[camello] += avance + + # Actualizar la representación visual (Thread-Safe) + bar = " " * min(posiciones[camello], meta) + + # Para la visualización, el camello siempre está al final de la barra + labels[camello].config(text=bar + "🐫" + " " * (meta - posiciones[camello]) + " | META") + + if posiciones[camello] >= meta: + ganador = camello + break + + if ganador: + resultado_label.config(text=f"¡{ganador} ha ganado la carrera!", fg='green') + config.juego_running = False + log_event(f"Carrera de camellos finalizada. Ganador: {ganador}") + + # Botón para cerrar + ttk.Button(juego_window, text="Cerrar Juego", command=cerrar_juego).pack(pady=10) + + elif config.juego_running: + # Re-programar el siguiente paso (Thread-Safe) + juego_window.after(300, avanzar_carrera) + + log_event("Simulación de Carrera de Camellos iniciada.") + juego_window.after(100, avanzar_carrera) # Iniciar la simulación \ No newline at end of file diff --git a/ui_layout.py b/ui_layout.py new file mode 100644 index 0000000..215cbe6 --- /dev/null +++ b/ui_layout.py @@ -0,0 +1,683 @@ +# ui_layout.py +import tkinter as tk +from tkinter import Menu, ttk, messagebox +from tkinter.scrolledtext import ScrolledText +import threading +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import datetime +import os + +# Importar funciones y variables +import system_utils +import monitor_manager +import config + + +def crear_ui_completa(root): + """Configura el layout principal, crea todos los widgets e inicia hilos.""" + + # Aplicar un tema más moderno para ttk + style = ttk.Style() + style.theme_use('clam') + + # --- FUNCIONES AUXILIARES DE UI (Para llamadas a eventos y botones) --- + + def abrir_editor_alarma(event_or_none, treeview_alarmas): + """ + Verifica la selección en el Treeview y abre la ventana flotante con los datos cargados. + Puede ser llamada por un evento (doble clic) o por un botón. + """ + # Obtenemos el ítem seleccionado (focus() funciona para botón y doble clic si hay foco) + selected_item = treeview_alarmas.focus() + + # Si no hay selección, salimos. + if not selected_item: + messagebox.showwarning("Advertencia", "Selecciona una alarma para modificar.") + return + + # Obtenemos el ID de la alarma (primer valor de la fila) + alarma_id = int(treeview_alarmas.item(selected_item, 'values')[0]) + data = config.alarmas_programadas.get(alarma_id) + + if not data: + messagebox.showerror("Error", "No se encontraron los datos de la alarma seleccionada.") + return + + # Llamamos a la función flotante en modo modificación + mostrar_selector_alarma_flotante(treeview_alarmas, alarma_id, data) + + + def on_closing(): + config.monitor_running = False + system_utils.detener_sonido_alarma() + system_utils.detener_mp3() # Detener música al cerrar + root.destroy() + + root.protocol("WM_DELETE_WINDOW", on_closing) + + # Configuración de Layout + root.columnconfigure(0, weight=0) + root.columnconfigure(1, weight=1) + root.columnconfigure(2, weight=0) + root.rowconfigure(0, weight=1) + root.rowconfigure(1, weight=0) + + # --- Creación de Frames Principales --- + frame_izquierdo = tk.Frame(root, bg="#f0f0f0", width=200) + frame_central = tk.Frame(root, bg="white") + frame_derecho = tk.Frame(root, bg="#f0f0f0", width=10) + + frame_izquierdo.grid(row=0, column=0, sticky="nsew") + frame_central.grid(row=0, column=1, sticky="nsew") + frame_derecho.grid(row=0, column=2, sticky="nsew") + + frame_izquierdo.grid_propagate(False) + frame_derecho.grid_propagate(False) + + # Layout del Frame Central + frame_central.rowconfigure(0, weight=1) + frame_central.rowconfigure(1, weight=0) + frame_central.columnconfigure(0, weight=1) + + frame_superior = tk.Frame(frame_central, bg="lightyellow") + frame_inferior = tk.Frame(frame_central, bg="lightgray", height=100) + + frame_superior.grid(row=0, column=0, sticky="nsew") + frame_inferior.grid(row=1, column=0, sticky="ew") + frame_inferior.grid_propagate(False) + + # --- Implementación del Progressbar (Frame Inferior) --- + progress_bar = ttk.Progressbar(frame_inferior, orient="horizontal", length=800, mode="determinate") + progress_bar.pack(pady=10, padx=20, fill="x") + config.progress_bar = progress_bar + # ----------------------------------------------- + + # Notebook para las pestañas + notebook = ttk.Notebook(frame_superior) + notebook.pack(fill="both", expand=True) + + # --- PESTAÑA 1: PROGRAMADOR DE ALARMAS --- + alarma_tab = ttk.Frame(notebook) + notebook.add(alarma_tab, text="Programador de Alarmas") + + # --- PESTAÑA 2: BLOC DE NOTAS --- + editor_tab = ttk.Frame(notebook) + notebook.add(editor_tab, text="Bloc de Notas") + + # --- PESTAÑA 3: MONITOR DEL SISTEMA (DEFINICIÓN ÚNICA) --- + monitor_tab = ttk.Frame(notebook) + notebook.add(monitor_tab, text="Monitor del Sistema", padding=4) + + # --- PESTAÑA 4: WEB SCRAPING --- + scraping_tab = ttk.Frame(notebook) + notebook.add(scraping_tab, text="Web Scraping") + + # --- PESTAÑA 5: JUEGOS --- + games_tab = ttk.Frame(notebook) + notebook.add(games_tab, text="Juegos 🎲") + + # --- PESTAÑA 6: MÚSICA (NUEVO) --- + music_tab = ttk.Frame(notebook) + notebook.add(music_tab, text="Música 🎵") + # --------------------------------------------- + + + # =============================================== + # FUNCIÓN PARA MOSTRAR EL SELECTOR DE ALARMA FLOTANTE + # =============================================== + def mostrar_selector_alarma_flotante(treeview_alarmas, alarma_id=None, data=None): + """ + Crea y muestra la interfaz de selección de hora flotante. + Si se proporciona alarma_id y data, funciona en modo MODIFICACIÓN. + """ + + is_modifying = alarma_id is not None + + popup = tk.Toplevel(root) + popup.title("Modificar Alarma" if is_modifying else "Añadir Alarma") + popup.geometry("450x300") # TAMAÑO AJUSTADO PARA VISIBILIDAD + popup.resizable(False, False) + popup.transient(root) + + # CORRECCIÓN CLAVE: Retrasar grab_set para evitar 'grab failed: window not viewable' + popup.after(10, popup.grab_set) + + # Variables de entrada (Carga de datos si es modo modificación) + initial_hora = data['time'].strftime("%H") if is_modifying else datetime.datetime.now().strftime("%H") + initial_minuto = data['time'].strftime("%M") if is_modifying else datetime.datetime.now().strftime("%M") + initial_tarea = data['message'] if is_modifying else "" + initial_sound_file = data['sound_file'] if is_modifying else config.ALERTA_SOUND_FILE + + # Inicializar con valores cargados o por defecto + hora_var = tk.StringVar(value=initial_hora) + minuto_var = tk.StringVar(value=initial_minuto) + tarea_var = tk.StringVar(value=initial_tarea) + + # --- Configuración del Label de Sonido --- + config.ALERTA_SOUND_FILE = initial_sound_file # Seteamos la global para la función seleccionar_archivo_alarma + initial_sound_text = os.path.basename(config.ALERTA_SOUND_FILE) if config.ALERTA_SOUND_FILE else "[No seleccionado]" + + main_frame = ttk.Frame(popup, padding="15") + main_frame.pack(fill='both', expand=True) + + # Grid para el layout + main_frame.columnconfigure(1, weight=1) + main_frame.columnconfigure(2, weight=1) + + # --- SECCIÓN HORA Y MINUTO --- + tk.Label(main_frame, text="Hora (HH):").grid(row=0, column=0, pady=5, sticky='w') + tk.Label(main_frame, text="Minuto (MM):").grid(row=0, column=2, pady=5, sticky='w') + + horas = [f"{h:02d}" for h in range(24)] + minutos = [f"{m:02d}" for m in range(60)] + + hora_cb = ttk.Combobox(main_frame, values=horas, textvariable=hora_var, width=5, state="readonly") + minuto_cb = ttk.Combobox(main_frame, values=minutos, textvariable=minuto_var, width=5, state="readonly") + + hora_cb.grid(row=1, column=0, padx=(5, 10), sticky='ew') + minuto_cb.grid(row=1, column=2, padx=(10, 5), sticky='ew') + + # --- SECCIÓN SONIDO --- + tk.Label(main_frame, text="Sonido:", font=('Helvetica', 9, 'bold')).grid(row=2, column=0, pady=(10, 5), sticky='w') + + label_archivo_seleccionado = tk.Label(main_frame, text=initial_sound_text, anchor='w') + label_archivo_seleccionado.grid(row=4, column=0, columnspan=3, padx=5, pady=(0, 0), sticky='ew') + + + # Botón para abrir el diálogo de selección de archivo + ttk.Button( + main_frame, + text="Seleccionar Audio (.wav/.mp3)", + command=lambda: system_utils.seleccionar_archivo_alarma(root, label_archivo_seleccionado) + ).grid(row=3, column=0, columnspan=3, padx=5, pady=(5, 5), sticky='ew') + + + # --- SECCIÓN MENSAJE --- + tk.Label(main_frame, text="Mensaje/Tarea:").grid(row=5, column=0, columnspan=3, pady=(10, 5), sticky='w') + tarea_entry = ttk.Entry(main_frame, textvariable=tarea_var, width=35) + tarea_entry.grid(row=6, column=0, columnspan=3, pady=5, sticky='ew') + + # Frame para botones de control (OK/CANCEL) + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=7, column=0, columnspan=3, pady=(15, 0), sticky='e') + + # --- Lógica de Comando --- + if is_modifying: + action_command = lambda: system_utils.modificar_alarma_existente( + root, alarma_id, hora_var.get(), minuto_var.get(), tarea_var.get(), treeview_alarmas, popup, config.ALERTA_SOUND_FILE + ) + action_text = "💾 Guardar Cambios" + else: + action_command = lambda: system_utils.agregar_alarma( + root, hora_var.get(), minuto_var.get(), tarea_var.get(), treeview_alarmas, popup, config.ALERTA_SOUND_FILE + ) + action_text = "➕ Añadir" + + # Botón OK (Programar/Modificar) + ttk.Button( + button_frame, + text=action_text, + width=18, + command=action_command + ).pack(side=tk.LEFT, padx=5) + + # Botón Cancelar + ttk.Button( + button_frame, + text="Cancelar", + width=10, + command=popup.destroy + ).pack(side=tk.LEFT) + + + # =============================================== + # CONTENIDO DE LA SOLAPA DE ALARMA + # =============================================== + main_alarm_frame = tk.Frame(alarma_tab) + main_alarm_frame.pack(fill="both", expand=True) + + # --- Lista de Alarmas (Treeview) --- + columns = ('ID', 'Hora', 'Tarea', 'Estado', 'Fecha') + treeview_alarmas = ttk.Treeview(main_alarm_frame, columns=columns, show='headings') + + for col in columns: + treeview_alarmas.heading(col, text=col, anchor=tk.W) + + treeview_alarmas.column('ID', width=40) + treeview_alarmas.column('Hora', width=40) + treeview_alarmas.column('Estado', width=70, anchor=tk.CENTER) + treeview_alarmas.column('Tarea', minwidth=150, stretch=tk.YES) + treeview_alarmas.column('Fecha', width=80) + treeview_alarmas.pack(fill='both', expand=True, padx=10, pady=10) + + # [NUEVO] Llamar a cargar alarmas DESPUÉS de crear el Treeview + system_utils.cargar_alarmas(treeview_alarmas, root) + + # --- Evento de Doble Clic para Modificar (CORREGIDO) --- + def handle_double_click(event): + abrir_editor_alarma(event, treeview_alarmas) + + treeview_alarmas.bind('', handle_double_click) + + # --- Panel de Control de Alarmas (Parte inferior) --- + control_frame = tk.LabelFrame(main_alarm_frame, text="Control", padx=15, pady=15) + control_frame.pack(fill='x', padx=10, pady=(0, 10)) + + # Botones principales + ttk.Button( + control_frame, + text="➕ Programar Nueva Alarma", + command=lambda: mostrar_selector_alarma_flotante(treeview_alarmas) # Llama a la función que abre el popup + ).pack(side=tk.LEFT, padx=10) + + # Botón Modificar (CORREGIDO: usa la función auxiliar) + ttk.Button( + control_frame, + text="✏️ Modificar Seleccionada", + command=lambda: abrir_editor_alarma(None, treeview_alarmas) + ).pack(side=tk.LEFT, padx=10) + + ttk.Button( + control_frame, + text="✅ Activar/Desactivar Seleccionada", + command=lambda: system_utils.toggle_alarma(treeview_alarmas) + ).pack(side=tk.LEFT, padx=10) + + ttk.Button( + control_frame, + text="🗑️ Eliminar Seleccionada", + command=lambda: system_utils.eliminar_alarma(treeview_alarmas) + ).pack(side=tk.LEFT, padx=10) + + # NUEVO: Separador + ttk.Separator(control_frame, orient='vertical').pack(side=tk.LEFT, padx=10, fill='y') + + # NUEVO: Control de Sonido + ttk.Button( + control_frame, + text="🔇 Detener Sonido Alarma", + command=system_utils.detener_sonido_alarma + ).pack(side=tk.LEFT, padx=10) + + tk.Label(control_frame, text="Volumen:").pack(side=tk.LEFT) + + volumen_scale = ttk.Scale( + control_frame, + from_=0, to=100, + orient='horizontal', + length=100, + command=system_utils.ajustar_volumen_alarma + ) + volumen_scale.set(config.alarma_volumen * 100) + volumen_scale.pack(side=tk.LEFT, padx=5) + + + # =============================================== + # CONTENIDO DE LA SOLAPA BLOC DE NOTAS + # =============================================== + + # Frame para los botones de control + control_frame_editor = tk.Frame(editor_tab) + control_frame_editor.pack(pady=5) + + # Botones del Editor + ttk.Button(control_frame_editor, text="Nuevo", command=system_utils.nuevo_archivo).pack(side=tk.LEFT, padx=5) + ttk.Button(control_frame_editor, text="Abrir TXT...", command=lambda: system_utils.abrir_archivo(root)).pack(side=tk.LEFT, padx=5) + ttk.Button(control_frame_editor, text="Guardar TXT", command=lambda: system_utils.guardar_texto(root)).pack(side=tk.LEFT, padx=15) + ttk.Button( + control_frame_editor, + text="📂 Abrir Carpeta Notas", # CAMBIO DE ETIQUETA + command=system_utils.abrir_carpeta_notas # LLAMA A LA FUNCIÓN DE NOTAS + ).pack(side=tk.LEFT, padx=5) + + # Widget de Texto + editor_text_widget = tk.Text(editor_tab, wrap='word', undo=True, font=('Courier New', 10)) + editor_text_widget.pack(fill="both", expand=True, padx=5, pady=5) + + # Asignar a la variable global + config.editor_texto = editor_text_widget + + + # --- Creación de la Barra de Estado --- + barra_estado = tk.Label(root, text="Barra de estado", bg="lightgray", anchor="w") + barra_estado.grid(row=1, column=0, columnspan=3, sticky="ew") + + label_1 = tk.Label(barra_estado, text="Estado Backup", bg="green", anchor="w", width=20) + label_2 = tk.Label(barra_estado, text="Registro: Detenido", bg="gray", anchor="w", width=20) + label_fecha_hora = tk.Label(barra_estado, text="Hilo fecha-hora", font=("Helvetica", 14), bd=1, fg="blue", relief="sunken", anchor="w", width=20, padx=10) + + label_1.pack(side="left", fill="x", expand=True) + label_2.pack(side="left", fill="x", expand=True) + label_fecha_hora.pack(side="right", fill="x", expand=True) + + # Asignar los labels a la configuración global para que los hilos los encuentren + config.label_1 = label_1 + config.label_2 = label_2 + config.label_fecha_hora = label_fecha_hora + + # --- Inicialización del Panel Lateral --- + monitor_manager.crear_panel_lateral(frame_izquierdo, root) + + # --- Creación del Menú Superior (SE MODIFICA) --- + menu_bar = Menu(root) + file_menu = Menu(menu_bar, tearoff=0); file_menu.add_command(label="Salir", command=on_closing) + + # MODIFICADO: Se elimina el comando de YouTube + launch_menu = Menu(menu_bar, tearoff=0); + launch_menu.add_command(label="ChatGPT", command=lambda: system_utils.lanzar_url("https://chat.openai.com")) + launch_menu.add_command(label="Apuntes PSP", command=lambda: system_utils.lanzar_url("https://apuntes-informatica.ieslamar.org/psp/proyecto")) + launch_menu.add_command(label="Solitario Google", command=lambda: system_utils.lanzar_url("https://www.google.com/logos/fnbx/solitaire/standalone.html")) + launch_menu.add_command(label="Aules FP", command=lambda: system_utils.lanzar_url("https://aules.edu.gva.es/fp/my/")) + + # MODIFICADO: Se ELIMINA el comando del juego thread-safe de este menú + tools_menu = Menu(menu_bar, tearoff=0); + tools_menu.add_command(label="Ejecutar Copia de Seguridad", command=lambda: system_utils.ejecutar_script_en_hilo(label_1, root)) + tools_menu.add_command(label="Iniciar/Detener Registro CSV", command=lambda: system_utils.manejar_registro_csv(label_2)) + # tools_menu.add_command(label="Simular Juego (Thread-Safe) 🐫", command=lambda: system_utils.simular_juego_camellos(root)) # ELIMINADO + tools_menu.add_command(label="📂 Abrir Carpeta Scraping", command=lambda: system_utils.abrir_carpeta_especifica(config.SCRAPING_FOLDER, "Scraping")) + tools_menu.add_command(label="📂 Abrir Config Scraping", command=lambda: system_utils.abrir_carpeta_especifica(config.SCRAPING_CONFIG_FOLDER, "Config Scraping")) + + menu_bar.add_cascade(label="Archivo", menu=file_menu) + menu_bar.add_cascade(label="Herramientas", menu=tools_menu) + menu_bar.add_cascade(label="Lanzadores", menu=launch_menu) + menu_bar.add_cascade(label="Ayuda", menu=Menu(menu_bar, tearoff=0)) + root.config(menu=menu_bar) + + + # =============================================== + # Solapa de Monitor del Sistema (CONTENIDO ÚNICO) + # =============================================== + + # Reutilizamos monitor_tab creado arriba para evitar la duplicidad + main_monitor_frame = tk.Frame(monitor_tab) + main_monitor_frame.pack(fill=tk.BOTH, expand=True) + main_monitor_frame.rowconfigure(0, weight=3) + main_monitor_frame.rowconfigure(1, weight=1) + main_monitor_frame.columnconfigure(0, weight=1) + + # --- Fila 0: Gráficos --- + plot_frame = tk.Frame(main_monitor_frame) + plot_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + + fig = plt.figure(figsize=(10, 8)) + gs = fig.add_gridspec(2, 3, hspace=0.6, wspace=0.3) + + ax_cpu = fig.add_subplot(gs[0, 0]) + ax_mem = fig.add_subplot(gs[0, 1]) + ax_cores = fig.add_subplot(gs[0, 2]) + ax_net = fig.add_subplot(gs[1, 0]) + ax_pie = fig.add_subplot(gs[1, 1], aspect="equal") + ax_disk_io = fig.add_subplot(gs[1, 2]) + + plt.style.use('ggplot') + + canvas = FigureCanvasTkAgg(fig, master=plot_frame) + canvas_widget = canvas.get_tk_widget() + canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + # --- Fila 1: Log y Procesos --- + bottom_frame = tk.Frame(main_monitor_frame) + bottom_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) + bottom_frame.columnconfigure(0, weight=1) + bottom_frame.columnconfigure(1, weight=1) + + # 1. Log de Eventos (Sección Izquierda) + log_frame = tk.LabelFrame(bottom_frame, text="Log de Eventos del Sistema") + log_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + log_frame.rowconfigure(0, weight=1) + log_frame.columnconfigure(0, weight=1) + + config.system_log = ScrolledText(log_frame, height=8, font=("Courier", 8), bg="#2c3e50", fg="lightgray") + config.system_log.grid(row=0, column=0, sticky="nsew") + + # 2. Treeview de Procesos (Sección Derecha) + process_frame = tk.LabelFrame(bottom_frame, text=f"Top {10} Procesos (Ordenados por CPU)") + process_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) + process_frame.columnconfigure(0, weight=1) + process_frame.rowconfigure(0, weight=1) + + columns = ('PID', 'CPU', 'MEM', 'HILOS', 'NOMBRE') + treeview_processes = ttk.Treeview(process_frame, columns=columns, show='headings') + + for col in columns: + treeview_processes.heading(col, text=col, anchor=tk.W) + treeview_processes.column(col, width=50, anchor=tk.W) + + treeview_processes.column('PID', width=50) + treeview_processes.column('CPU', width=60) + treeview_processes.column('MEM', width=70) + treeview_processes.column('HILOS', width=60) + treeview_processes.column('NOMBRE', minwidth=150, stretch=tk.YES) + + treeview_processes.grid(row=0, column=0, sticky="nsew") + + kill_button = tk.Button( + process_frame, + text="Terminar Proceso Seleccionado (⚠️ DANGER)", + command=lambda: monitor_manager.terminar_proceso(treeview_processes), + bg='darkred', + fg='white', + font=('Helvetica', 10, 'bold') + ) + kill_button.grid(row=1, column=0, sticky="ew", pady=(5,0)) + + # =============================================== + # Solapa de Web Scraping (Implementación completa y estética) + # =============================================== + + # Marco principal para el scraping + main_scraping_frame = ttk.Frame(scraping_tab, padding="15") + main_scraping_frame.pack(fill=tk.BOTH, expand=True) + + main_scraping_frame.columnconfigure(0, weight=1) + main_scraping_frame.columnconfigure(1, weight=0) + main_scraping_frame.rowconfigure(5, weight=1) # Fila 5 es el Text Output + + # --- Fila 0 & 1: URL, Opciones y Configuración Personalizada --- + + # 1. Título y Carga de Configuración + header_frame = ttk.Frame(main_scraping_frame) + header_frame.grid(row=0, column=0, columnspan=2, sticky='ew', pady=(0, 5)) + header_frame.columnconfigure(0, weight=1) + + # CORRECCIÓN DE ORTOGRAFÍA: "Scrappear" -> "Scrapear" + ttk.Label(header_frame, text="🌐 URL a Scrapear:", font=('Helvetica', 10, 'bold')).pack(side=tk.LEFT, padx=(0, 5)) + + config.scraping_config_file_label = ttk.Label(header_frame, text="Config: [Ninguna]", foreground='blue') + config.scraping_config_file_label.pack(side=tk.RIGHT, padx=5) + + ttk.Button( + header_frame, + text="⚙️ Cargar Config. (.json)", + command=lambda: system_utils.abrir_archivo_scraping_config(root) + ).pack(side=tk.RIGHT) + + # 2. Entrada de URL + url_var = tk.StringVar(value="https://www.example.com") + url_entry = ttk.Entry(main_scraping_frame, textvariable=url_var, width=80) + url_entry.grid(row=1, column=0, columnspan=2, sticky='ew', pady=(0, 10)) + + # ASIGNAR A CONFIG para que system_utils pueda actualizarlo + config.scraping_url_input = url_var + + # --- Fila 2: Controles Detallados --- + control_frame_row2 = ttk.Frame(main_scraping_frame) + control_frame_row2.grid(row=2, column=0, columnspan=2, sticky='ew', pady=5) + control_frame_row2.columnconfigure(2, weight=1) + + # Opciones de Extracción + ttk.Label(control_frame_row2, text="Tipo de Extracción:", font=('Helvetica', 9, 'bold')).grid(row=0, column=0, sticky='w', padx=(0, 5)) + tipo_extraccion_var = tk.StringVar(value="Título y Metadatos") + extracciones = [ + "Título y Metadatos", + "Primeros Párrafos", + "Enlaces (Links)", + "Imágenes (URLs)", + "Tablas (Estructura Básica)", + "Portátiles Gamer (Enlace + Precio)", # NUEVA OPCIÓN COMBINADA + "-> Texto Específico (CSS Selector)", + "-> Atributo Específico (CSS Selector + Attr)" + ] + + extraccion_combobox = ttk.Combobox( + control_frame_row2, + values=extracciones, + textvariable=tipo_extraccion_var, + state="readonly", + width=30 + ) + extraccion_combobox.grid(row=0, column=1, sticky='w', padx=10) + + # Selector CSS + ttk.Label(control_frame_row2, text="Selector CSS/Tag (avanzado):").grid(row=0, column=3, sticky='w', padx=(10, 5)) + config.scraping_selector_input = ttk.Entry(control_frame_row2, width=40) + config.scraping_selector_input.grid(row=0, column=4, sticky='ew', padx=(0, 10)) + + # Atributo + ttk.Label(control_frame_row2, text="Atributo (ej: href/src):").grid(row=0, column=5, sticky='w', padx=(10, 5)) + config.scraping_attr_input = ttk.Entry(control_frame_row2, width=15) + config.scraping_attr_input.grid(row=0, column=6, sticky='ew') + + # --- Fila 3: Ejecución y Control --- + control_execution_frame = ttk.Frame(main_scraping_frame) + control_execution_frame.grid(row=3, column=0, columnspan=2, sticky='ew', pady=(10, 5)) + control_execution_frame.columnconfigure(0, weight=1) + + # Botón Scrappear + btn_scrap = ttk.Button( + control_execution_frame, + text="🚀 Iniciar Scrapear", + command=lambda: monitor_manager.scrappear_pagina_principal( + url_var.get(), + tipo_extraccion_var.get(), + config.scraping_output_text, + config.scraping_progress_bar, + config.scraping_selector_input.get(), + config.scraping_attr_input.get(), + config.scraping_config_data, + root + ) + ) + btn_scrap.pack(side=tk.LEFT, padx=(0, 10)) + + # Barra de Progreso + config.scraping_progress_bar = ttk.Progressbar(control_execution_frame, orient="horizontal", mode="indeterminate") + config.scraping_progress_bar.pack(side=tk.LEFT, fill='x', expand=True, padx=(0, 10)) + + # Botón Detener + ttk.Button( + control_execution_frame, + text="🛑 Detener", + command=lambda: system_utils.detener_scraping() + ).pack(side=tk.LEFT) + + # --- Fila 4 & 5: Área de Resultado y Guardar --- + + ttk.Label(main_scraping_frame, text="📊 Resultado de la Extracción:", font=('Helvetica', 10, 'bold')).grid(row=4, column=0, columnspan=2, sticky='w', pady=(10, 5)) + + # Widget de Texto para la salida + config.scraping_output_text = ScrolledText(main_scraping_frame, wrap='word', font=('Courier New', 9), height=18, bg='#f9f9f9') + config.scraping_output_text.grid(row=5, column=0, columnspan=2, sticky='nsew', pady=(0, 10)) + + # Botón Guardar Resultado + ttk.Button( + main_scraping_frame, + text="💾 Guardar Resultado en /data/scraping", # CORRECCIÓN DE RUTA + command=lambda: system_utils.guardar_scraping(config.scraping_output_text.get("1.0", tk.END), root) + ).grid(row=6, column=0, columnspan=2, sticky='ew') + + + # =============================================== + # CONTENIDO DE LA SOLAPA DE JUEGOS + # =============================================== + + main_games_frame = ttk.Frame(games_tab, padding="20") + main_games_frame.pack(fill=tk.BOTH, expand=True) + + ttk.Label( + main_games_frame, + text="Simulaciones de Entretenimiento (Thread-Safe)", + font=('Helvetica', 14, 'bold') + ).pack(pady=15) + + ttk.Separator(main_games_frame, orient='horizontal').pack(fill='x', pady=5) + + # Botón de juego de camellos + ttk.Button( + main_games_frame, + text="🏆 Iniciar Carrera de Camellos (Thread-Safe)", + command=lambda: system_utils.simular_juego_camellos(root), + cursor="hand2" + ).pack(pady=10) + + ttk.Label( + main_games_frame, + text="Este juego se ejecuta en una ventana 'Toplevel' para garantizar la seguridad del hilo principal.", + font=('Helvetica', 9, 'italic'), + foreground='gray' + ).pack(pady=5) + + # =============================================== + # CONTENIDO DE LA SOLAPA DE MÚSICA (NUEVO) + # =============================================== + + music_frame = ttk.Frame(music_tab, padding="20") + music_frame.pack(fill=tk.BOTH, expand=True) + + ttk.Label( + music_frame, + text="Reproductor de Audio Local (.mp3 / .wav)", + font=('Helvetica', 14, 'bold') + ).pack(pady=15) + + # 1. Archivo Seleccionado + ttk.Label(music_frame, text="Archivo Seleccionado:").pack(pady=(10, 5)) + label_music_file = ttk.Label(music_frame, text="[Ningún archivo cargado]", anchor='center', foreground='blue') + label_music_file.pack(fill='x', padx=50) + + # 2. Botón Seleccionar + ttk.Button( + music_frame, + text="📂 Seleccionar MP3/WAV", + command=lambda: system_utils.seleccionar_mp3(root, label_music_file) + ).pack(pady=10, padx=50, fill='x') + + ttk.Separator(music_frame, orient='horizontal').pack(fill='x', pady=10, padx=50) + + # 3. Controles de Reproducción + control_music_frame = ttk.Frame(music_frame) + control_music_frame.pack(pady=10) + + ttk.Button( + control_music_frame, + text="▶️ Reproducir", + command=lambda: system_utils.reproducir_mp3(root) + ).pack(side=tk.LEFT, padx=10) + + ttk.Button( + control_music_frame, + text="⏹️ Detener", + command=system_utils.detener_mp3 + ).pack(side=tk.LEFT, padx=10) + + # 4. Control de Volumen + ttk.Label(music_frame, text="Volumen:").pack(pady=(15, 5)) + + volumen_scale_music = ttk.Scale( + music_frame, + from_=0, to=100, + orient='horizontal', + length=200, + command=system_utils.ajustar_volumen_mp3 # Reutilizamos la función que ajusta Pygame Mixer + ) + volumen_scale_music.set(config.alarma_volumen * 100) + volumen_scale_music.pack(pady=5) + + # --- Iniciar Hilos --- + system_utils.log_event("Monitor de sistema iniciado. Esperando la primera lectura de métricas...") + monitor_manager.iniciar_monitor_sistema(fig, canvas, ax_cpu, ax_mem, ax_net, ax_cores, ax_pie, ax_disk_io, treeview_processes, root) + + update_thread = threading.Thread(target=lambda: system_utils.update_time(label_fecha_hora, root)) + update_thread.daemon = True + update_thread.start() \ No newline at end of file