Version FInal

This commit is contained in:
Luka 2025-12-06 19:09:50 +01:00
commit 401fe152e6
19 changed files with 2803 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

35
backup_script.sh Executable file
View File

@ -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

79
config.py Normal file
View File

@ -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

17
data/alarmas.json Normal file
View File

@ -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"
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
Mireya Serrano es una crack, texto de ejemplo

View File

@ -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
1 Timestamp CPU_Total (%) RAM_Total (%) Net_Sent (KB/s) Net_Recv (KB/s)
2 2025-11-29 14:04:14 14.1 18.3 104.3623046875 80.29296875
3 2025-11-29 14:04:15 17.3 18.4 103.626953125 63.369140625
4 2025-11-29 14:04:16 17.5 18.4 97.59765625 285.9462890625
5 2025-11-29 14:04:18 14.8 18.4 86.7509765625 6.6787109375
6 2025-11-29 14:04:19 15.0 18.4 148.703125 6.470703125
7 2025-11-29 14:04:20 13.8 18.4 69.0546875 116.576171875
8 2025-11-29 14:04:21 15.7 18.5 89.3642578125 68.330078125
9 2025-11-29 14:04:23 13.3 18.5 104.0947265625 62.2880859375

View File

@ -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 ---

View File

@ -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 ---

View File

@ -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."
}

View File

@ -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."
}

553
monitor_manager.py Normal file
View File

@ -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 (<img>).\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 (<table>).\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()

19
proyecto.py Normal file
View File

@ -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()

963
system_utils.py Normal file
View File

@ -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

683
ui_layout.py Normal file
View File

@ -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('<Double-1>', 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()