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