Compare commits
No commits in common. "e2276966151cffeda2d2ca3251cc9a4a9a19599c" and "947b67610c02eb6326400cb480807306a23afc3c" have entirely different histories.
e227696615
...
947b67610c
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,35 +0,0 @@
|
||||||
#!/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
79
config.py
|
|
@ -1,79 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"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.
|
|
@ -1 +0,0 @@
|
||||||
Mireya Serrano es una crack, texto de ejemplo
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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,373 +0,0 @@
|
||||||
--- 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 ---
|
|
||||||
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
--- 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 ---
|
|
||||||
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
|
|
@ -1,553 +0,0 @@
|
||||||
# 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
19
proyecto.py
|
|
@ -1,19 +0,0 @@
|
||||||
# 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
963
system_utils.py
|
|
@ -1,963 +0,0 @@
|
||||||
# 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
683
ui_layout.py
|
|
@ -1,683 +0,0 @@
|
||||||
# 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()
|
|
||||||
Loading…
Reference in New Issue