3.1.0: se instala solo en su sitio, manual dentro y el puerto para una sola copia
- instalar.py: instalar un paquete lo copia a la carpeta de programas del usuario (%LOCALAPPDATA%\Programs\OrganiLAB) con sus datos y su ffmpeg, lo da de alta desde ahí y lo vuelve a abrir desde su sitio. Del origen no se borra nada: queda instalado-en.txt, que además hace que abrir esa copia abra la instalada. Un destino ocupado no se pisa, y si ya hay un OrganiLAB instalado lo dice en vez de poner otro encima. - static/index.html: vista «Manual», con cada función del programa, qué hace y cómo lo hace (41 entradas, un apartado por pestaña). - organizador.py: ServidorLocal pide el puerto en exclusiva. Con «reutilizar dirección», en Windows dos copias escuchaban a la vez en el mismo puerto y se contestaban la una por la otra sin que nada fallara a la vista. - Pruebas: probar.py 19, prueba_funcional.py 26 de 26, prueba_ventana.py 18 desde el código y 17 con el paquete, incluida la instalación entera en un equipo de ensayo (ORGANILAB_ENSAYO) que no toca el Windows de verdad. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
45
README.md
45
README.md
@@ -7,17 +7,17 @@ menú Inicio y todo ocurre en este equipo.
|
|||||||
```
|
```
|
||||||
organizador.py ← inventario + plan + cola de conversión, y el servidor local con que habla la interfaz
|
organizador.py ← inventario + plan + cola de conversión, y el servidor local con que habla la interfaz
|
||||||
ventana.py ← la ventana de Windows del programa (marco Win32 + WebView2, solo ctypes)
|
ventana.py ← la ventana de Windows del programa (marco Win32 + WebView2, solo ctypes)
|
||||||
instalar.py ← accesos directos en Inicio y Escritorio, y la línea en Configuración › Aplicaciones
|
instalar.py ← lo instala en su sitio (carpeta de programas del usuario), con accesos en Inicio y Escritorio y su línea en Aplicaciones
|
||||||
arranque.py ← cómo arranca el paquete: su .exe es el Python firmado, y esto le dice qué hacer
|
arranque.py ← cómo arranca el paquete: su .exe es el Python firmado, y esto le dice qué hacer
|
||||||
reglas.py ← cómo reconoce cámaras, carpetas, basura y las plantillas de estructura
|
reglas.py ← cómo reconoce cámaras, carpetas, basura y las plantillas de estructura
|
||||||
resolve_bridge.py ← convierte BRAW a H.265 10 bits usando DaVinci Resolve Studio
|
resolve_bridge.py ← convierte BRAW a H.265 10 bits usando DaVinci Resolve Studio
|
||||||
actualizador.py ← busca versión nueva en Labstream OS (solo si se le pide) y la instala; firma.py la verifica
|
actualizador.py ← busca versión nueva en Labstream OS (solo si se le pide) y la instala; firma.py la verifica
|
||||||
static/index.html ← la interfaz (Carpeta · Revisar y ordenar · Mapa · Convertidor · Respaldar · Historial · Ajustes)
|
static/index.html ← la interfaz (Carpeta · Revisar y ordenar · Mapa · Convertidor · Respaldar · Historial · Ajustes · Manual)
|
||||||
static/manrope.woff2 ← la tipografía, para no pedirle nada a nadie fuera de este equipo
|
static/manrope.woff2 ← la tipografía, para no pedirle nada a nadie fuera de este equipo
|
||||||
WebView2Loader.dll ← la entrada a WebView2 (Microsoft, firmada); lo único que viaja y no es de Python
|
WebView2Loader.dll ← la entrada a WebView2 (Microsoft, firmada); lo único que viaja y no es de Python
|
||||||
datos/ ← inventario.sqlite, config.json, diccionario.json (lo aprendido), bitacora.jsonl
|
datos/ ← inventario.sqlite, config.json, diccionario.json (lo aprendido), bitacora.jsonl
|
||||||
empaquetar.py ← arma el paquete: dist\OrganizadorLabstream\ con OrganizadorLabstream.exe y los dos zips
|
empaquetar.py ← arma el paquete: dist\OrganizadorLabstream\ con OrganizadorLabstream.exe y los dos zips
|
||||||
probar.py ← 17 pruebas sin abrir nada · prueba_funcional.py: las 26 funciones · prueba_ventana.py: la ventana de verdad
|
probar.py ← 19 pruebas sin abrir nada · prueba_funcional.py: las 26 funciones · prueba_ventana.py: la ventana de verdad
|
||||||
Iniciar.bat ← desde el código, con consola (para ver lo que imprime)
|
Iniciar.bat ← desde el código, con consola (para ver lo que imprime)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -696,6 +696,45 @@ una sola copia, puerto ocupado, Windows sin WebView2, las tablas de la ventana,
|
|||||||
`prueba_funcional.py` sigue en 26 de 26; y `prueba_ventana.py`, nueva, abre la ventana de verdad y
|
`prueba_funcional.py` sigue en 26 de 26; y `prueba_ventana.py`, nueva, abre la ventana de verdad y
|
||||||
comprueba 17 cosas desde el código y 8 más arrancando el paquete por su .exe.
|
comprueba 17 cosas desde el código y 8 más arrancando el paquete por su .exe.
|
||||||
|
|
||||||
|
3.1.0, instalar es instalar de verdad, y el manual va dentro.
|
||||||
|
|
||||||
|
**El programa se instala solo en su sitio.** En la 3.0 «instalar» daba de alta el programa donde estuviera la
|
||||||
|
carpeta, y la carpeta suele estar en Descargas, que es de donde se limpia: en el equipo de Munka hubo que
|
||||||
|
mudarla a mano. Ahora, la primera vez que se abre un paquete, la pregunta es «¿Instalar OrganiLAB en este
|
||||||
|
equipo?», y decir que sí lo copia a `%LOCALAPPDATA%\Programs\OrganiLAB` con sus datos y su ffmpeg, lo da de
|
||||||
|
alta desde ahí (Inicio, Escritorio, Aplicaciones), lo cierra y lo vuelve a abrir desde su sitio
|
||||||
|
(`instalar.poner`, `instalar.mudar`). Lo mismo hace `OrganizadorLabstream.exe instalar`. Detalles:
|
||||||
|
|
||||||
|
- Del origen no se borra ni se mueve nada. Queda `instalado-en.txt`, que dice adónde se fue y además hace
|
||||||
|
que abrir esa copia abra la instalada (`instalar.mudado`): no hay dos programas con dos juegos de datos.
|
||||||
|
- No viaja lo que es de la sesión abierta (`sesion.json`) ni lo que se rehace solo (el perfil del motor de la
|
||||||
|
ventana, `actualizacion\`, `__pycache__`). El inventario se copia con la copia de SQLite, que sale entera
|
||||||
|
y coherente aunque el programa esté abierto.
|
||||||
|
- Un destino que ya existe y no está vacío no se pisa. Si ya hay un OrganiLAB instalado en el equipo, lo
|
||||||
|
dice y no instala otro encima. Si falta sitio en el disco o la copia falla a medias, quita lo copiado y
|
||||||
|
el programa sigue funcionando desde donde estaba.
|
||||||
|
- Si el paquete ya está en su sitio, o se corre desde el código, instalar sigue siendo solo darlo de alta.
|
||||||
|
- Para probarlo sin tocar el Windows de verdad, `ORGANILAB_ENSAYO=<carpeta>` hace que el «equipo» sea esa
|
||||||
|
carpeta (programas, Inicio, Escritorio y una clave aparte del registro). `prueba_ventana.py --paquete` lo
|
||||||
|
usa para recorrer la instalación entera: abre desde unas «Descargas», acepta la oferta, espera a que se
|
||||||
|
reabra desde su sitio y comprueba que abrir la copia de origen abre la instalada.
|
||||||
|
|
||||||
|
**El puerto, para una sola copia.** Probando la instalación salió un fallo que venía de siempre:
|
||||||
|
`ThreadingHTTPServer` pide «reutilizar dirección», y en Windows eso deja que dos programas escuchen a la vez
|
||||||
|
en el mismo puerto; las peticiones van a cualquiera de los dos. Dos OrganiLAB con datos distintos (el
|
||||||
|
instalado y una copia suelta) se contestaban el uno por el otro sin que nada fallara a la vista, y la
|
||||||
|
detección de «ya hay uno abierto» por puerto ocupado no saltaba nunca. `ServidorLocal` pide el puerto en
|
||||||
|
exclusiva (`SO_EXCLUSIVEADDRUSE`); si está ocupado, la copia toma otro. La prueba del puerto ocupado usa
|
||||||
|
ahora un servidor como el de otro OrganiLAB, que es justo el caso que no fallaba cuando debía. Y las pruebas
|
||||||
|
con ventana cierran siempre las copias que abren, también cuando una comprobación falla.
|
||||||
|
|
||||||
|
**Manual.** Vista nueva en la interfaz, «Manual», junto a Ajustes: cada función del programa, qué hace y
|
||||||
|
cómo lo hace, en el orden de las pestañas (41 entradas en 12 apartados). Vive en `static/index.html`, así
|
||||||
|
que viaja con cada versión; una prueba comprueba que cada pestaña tiene su apartado.
|
||||||
|
|
||||||
|
Pruebas: `probar.py` 19 de 19, `prueba_funcional.py` 26 de 26, `prueba_ventana.py` 18 desde el código y,
|
||||||
|
con el paquete, 8 del arranque y 9 de la instalación.
|
||||||
|
|
||||||
Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol
|
Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol
|
||||||
exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó
|
exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó
|
||||||
con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:
|
con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:
|
||||||
|
|||||||
@@ -161,8 +161,10 @@ with open(os.path.join(dest, "Apartar stock Envato viejo (arrastra aqui).bat"),
|
|||||||
with open(os.path.join(dest, "LEEME.txt"), "w", encoding="utf-8") as fh:
|
with open(os.path.join(dest, "LEEME.txt"), "w", encoding="utf-8") as fh:
|
||||||
fh.write(f"OrganiLAB · Organizador Labstream {VERSION}\n\n"
|
fh.write(f"OrganiLAB · Organizador Labstream {VERSION}\n\n"
|
||||||
"ABRIRLO: doble clic en OrganizadorLabstream.exe. Se abre en su propia ventana, como cualquier programa de Windows.\n"
|
"ABRIRLO: doble clic en OrganizadorLabstream.exe. Se abre en su propia ventana, como cualquier programa de Windows.\n"
|
||||||
"La primera vez pregunta si lo quieres en el menú Inicio y en el Escritorio; desde entonces se abre desde ahí\n"
|
"La primera vez pregunta si lo instalas: se copia a tu carpeta de programas, con sus datos, y queda en el menú\n"
|
||||||
"(y se quita en Configuración › Aplicaciones). También: OrganizadorLabstream.exe instalar / desinstalar.\n\n"
|
"Inicio y en el Escritorio. Desde entonces se abre desde ahí, y esta carpeta ya no hace falta: se puede borrar.\n"
|
||||||
|
"Se quita en Configuración › Aplicaciones. También: OrganizadorLabstream.exe instalar / desinstalar.\n\n"
|
||||||
|
"MANUAL: dentro del programa, arriba a la derecha, «Manual»: cada función, qué hace y cómo lo hace.\n\n"
|
||||||
"En el Explorador el .exe sale con el icono de Python: es el motor que lo mueve, firmado por la Python Software\n"
|
"En el Explorador el .exe sale con el icono de Python: es el motor que lo mueve, firmado por la Python Software\n"
|
||||||
"Foundation, y por eso Windows lo deja abrir también con Smart App Control encendido. En Inicio, en el Escritorio\n"
|
"Foundation, y por eso Windows lo deja abrir también con Smart App Control encendido. En Inicio, en el Escritorio\n"
|
||||||
"y en la barra de tareas sale con su icono.\n\n"
|
"y en la barra de tareas sale con su icono.\n\n"
|
||||||
|
|||||||
148
instalar.py
148
instalar.py
@@ -1,11 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
r"""
|
r"""
|
||||||
OrganiLAB como un programa más de Windows: su acceso directo en el menú Inicio y en el Escritorio, y su
|
OrganiLAB como un programa más de Windows: en su sitio, con su acceso directo en el menú Inicio y en el
|
||||||
línea en Configuración › Aplicaciones para quitarlo.
|
Escritorio, y con su línea en Configuración › Aplicaciones para quitarlo.
|
||||||
|
|
||||||
No copia nada: el programa se queda en la carpeta donde está y esto solo lo da de alta, todo dentro del
|
Instalar el paquete lo lleva a su sitio definitivo, la carpeta de programas del usuario
|
||||||
usuario y sin pedir permisos de administrador. Quitarlo borra los accesos directos y la línea de
|
(%LOCALAPPDATA%\Programs\OrganiLAB): de Descargas, que es donde se suele descomprimir, se limpia por
|
||||||
Aplicaciones; la carpeta del programa y los datos no se tocan.
|
descuido. Se copia el programa con sus datos y su ffmpeg; del origen no se borra ni se mueve nada, y allí
|
||||||
|
queda una nota que, además, hace que abrir esa copia abra la instalada. Todo dentro del usuario, sin
|
||||||
|
permisos de administrador. Desde el código (sin .exe) solo se da de alta donde está.
|
||||||
|
|
||||||
|
Quitarlo borra los accesos directos y la línea de Aplicaciones; la carpeta del programa y los datos no se
|
||||||
|
tocan.
|
||||||
|
|
||||||
Los accesos directos llevan la misma identidad que la ventana (ventana.IDENTIDAD). Así el acceso anclado
|
Los accesos directos llevan la misma identidad que la ventana (ventana.IDENTIDAD). Así el acceso anclado
|
||||||
a la barra de tareas y la ventana abierta son, para Windows, la misma cosa: un solo botón, con su icono.
|
a la barra de tareas y la ventana abierta son, para Windows, la misma cosa: un solo botón, con su icono.
|
||||||
@@ -15,15 +20,26 @@ a la barra de tareas y la ventana abierta son, para Windows, la misma cosa: un s
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import ctypes
|
import ctypes
|
||||||
|
import shutil
|
||||||
import winreg
|
import winreg
|
||||||
|
import sqlite3
|
||||||
from ctypes import wintypes as W
|
from ctypes import wintypes as W
|
||||||
|
|
||||||
import ventana as V
|
import ventana as V
|
||||||
|
|
||||||
NOMBRE = "OrganiLAB"
|
NOMBRE = "OrganiLAB"
|
||||||
DESCRIPCION = "OrganiLAB · ordena y aligera el material de video"
|
DESCRIPCION = "OrganiLAB · ordena y aligera el material de video"
|
||||||
CLAVE = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\OrganiLAB"
|
# Para probar la instalación entera sin tocar el Windows de verdad: con ORGANILAB_ENSAYO=<carpeta>, el
|
||||||
|
# «equipo» es esa carpeta (sus programas, su menú Inicio y su Escritorio) y la línea de Aplicaciones va a
|
||||||
|
# una clave aparte del registro. Lo usa prueba_ventana.py.
|
||||||
|
ENSAYO = os.environ.get("ORGANILAB_ENSAYO", "")
|
||||||
|
CLAVE = r"Software\OrganiLAB-ensayo\Alta" if ENSAYO else r"Software\Microsoft\Windows\CurrentVersion\Uninstall\OrganiLAB"
|
||||||
|
MARCA_MUDANZA = "instalado-en.txt" # en la carpeta de origen: adónde se instaló lo que había aquí
|
||||||
|
INVENTARIO = "inventario.sqlite"
|
||||||
|
NO_VIAJA = {"actualizacion", "__pycache__", MARCA_MUDANZA} # del primer nivel: lo que es de paso
|
||||||
|
NO_VIAJA_DATOS = {"sesion.json", "ventana"} # de datos\: la sesión abierta y el perfil del motor, que se rehace solo
|
||||||
EN_INICIO = "{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}" # la carpeta Programas del menú Inicio
|
EN_INICIO = "{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}" # la carpeta Programas del menú Inicio
|
||||||
EN_ESCRITORIO = "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}" # el Escritorio (puede estar en OneDrive)
|
EN_ESCRITORIO = "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}" # el Escritorio (puede estar en OneDrive)
|
||||||
CLSID_ACCESO, IID_ACCESO = "{00021401-0000-0000-C000-000000000046}", "{000214F9-0000-0000-C000-000000000046}"
|
CLSID_ACCESO, IID_ACCESO = "{00021401-0000-0000-C000-000000000046}", "{000214F9-0000-0000-C000-000000000046}"
|
||||||
@@ -132,9 +148,106 @@ def leer_acceso(ruta):
|
|||||||
|
|
||||||
def _accesos():
|
def _accesos():
|
||||||
"""Las dos rutas donde van los accesos directos: (menú Inicio, Escritorio)."""
|
"""Las dos rutas donde van los accesos directos: (menú Inicio, Escritorio)."""
|
||||||
|
if ENSAYO:
|
||||||
|
carpetas = os.path.join(ENSAYO, "Inicio"), os.path.join(ENSAYO, "Escritorio")
|
||||||
|
for c in carpetas:
|
||||||
|
os.makedirs(c, exist_ok=True)
|
||||||
|
return tuple(os.path.join(c, NOMBRE + ".lnk") for c in carpetas)
|
||||||
return os.path.join(carpeta_de(EN_INICIO), NOMBRE + ".lnk"), os.path.join(carpeta_de(EN_ESCRITORIO), NOMBRE + ".lnk")
|
return os.path.join(carpeta_de(EN_INICIO), NOMBRE + ".lnk"), os.path.join(carpeta_de(EN_ESCRITORIO), NOMBRE + ".lnk")
|
||||||
|
|
||||||
|
|
||||||
|
def _misma(a, b):
|
||||||
|
return os.path.normcase(os.path.abspath(a)) == os.path.normcase(os.path.abspath(b))
|
||||||
|
|
||||||
|
|
||||||
|
def sitio_definitivo():
|
||||||
|
"""Donde Windows pone los programas de cada usuario. De ahí no se limpia nada por descuido."""
|
||||||
|
programas = os.path.join(ENSAYO, "Programs") if ENSAYO else os.path.join(
|
||||||
|
os.environ.get("LOCALAPPDATA") or os.path.expanduser("~\\AppData\\Local"), "Programs")
|
||||||
|
return os.path.join(programas, NOMBRE)
|
||||||
|
|
||||||
|
|
||||||
|
def hay_que_mudar(base):
|
||||||
|
"""True si instalar significa llevar el programa a su sitio: es un paquete (trae su .exe) y no está ya allí."""
|
||||||
|
return os.path.isfile(os.path.join(base, "OrganizadorLabstream.exe")) and not _misma(base, sitio_definitivo())
|
||||||
|
|
||||||
|
|
||||||
|
def mudado(base):
|
||||||
|
"""Si desde esta carpeta ya se instaló el programa en otra, el .exe de esa otra; si no, ""."""
|
||||||
|
try:
|
||||||
|
with open(os.path.join(base, MARCA_MUDANZA), encoding="utf-8") as fh:
|
||||||
|
destino = fh.readline().strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
exe = os.path.join(destino, "OrganizadorLabstream.exe")
|
||||||
|
return exe if destino and not _misma(destino, base) and os.path.isfile(exe) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _lo_que_viaja(base):
|
||||||
|
"""(ruta relativa, bytes) de cada archivo que se lleva la mudanza. No viaja lo que es de la sesión abierta
|
||||||
|
ni lo que se rehace solo; el inventario va aparte, con la copia segura de SQLite."""
|
||||||
|
out = []
|
||||||
|
for raiz, dirs, files in os.walk(base):
|
||||||
|
rel = os.path.relpath(raiz, base)
|
||||||
|
if rel == ".":
|
||||||
|
dirs[:] = [d for d in dirs if d not in NO_VIAJA]
|
||||||
|
files = [f for f in files if f not in NO_VIAJA]
|
||||||
|
elif rel == "datos":
|
||||||
|
dirs[:] = [d for d in dirs if d not in NO_VIAJA_DATOS]
|
||||||
|
files = [f for f in files if f not in NO_VIAJA_DATOS and not f.startswith(INVENTARIO) and not f.endswith(".tmp")]
|
||||||
|
for f in files:
|
||||||
|
p = os.path.join(raiz, f)
|
||||||
|
out.append((os.path.relpath(p, base), os.path.getsize(p)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def mudar(base, destino=None, avance=None):
|
||||||
|
"""Copia el programa de `base`, con sus datos y su ffmpeg, a su sitio definitivo. Del origen no borra ni
|
||||||
|
mueve nada: deja allí la nota de la mudanza. Si falla a medias, quita lo copiado y el origen sigue como
|
||||||
|
estaba. `avance(0..1)` va diciendo cuánto lleva. Devuelve el destino."""
|
||||||
|
destino = destino or sitio_definitivo()
|
||||||
|
if os.path.isdir(destino) and os.listdir(destino):
|
||||||
|
raise FileExistsError(f"la carpeta {destino} ya existe y no está vacía")
|
||||||
|
cosas = _lo_que_viaja(base)
|
||||||
|
inventario = os.path.join(base, "datos", INVENTARIO)
|
||||||
|
total = sum(t for _, t in cosas) + (os.path.getsize(inventario) if os.path.exists(inventario) else 0) or 1
|
||||||
|
os.makedirs(os.path.dirname(destino), exist_ok=True)
|
||||||
|
if shutil.disk_usage(os.path.dirname(destino)).free < total * 1.05 + (50 << 20):
|
||||||
|
raise OSError(f"no hay sitio en el disco: hacen falta {total / 1e6:.0f} MB")
|
||||||
|
hechos = 0
|
||||||
|
try:
|
||||||
|
for rel, tam in cosas:
|
||||||
|
a = os.path.join(destino, rel)
|
||||||
|
os.makedirs(os.path.dirname(a), exist_ok=True)
|
||||||
|
shutil.copy2(os.path.join(base, rel), a)
|
||||||
|
hechos += tam
|
||||||
|
if avance:
|
||||||
|
avance(hechos / total)
|
||||||
|
if os.path.exists(inventario): # puede estar en uso: SQLite sabe copiarse entero y coherente
|
||||||
|
os.makedirs(os.path.join(destino, "datos"), exist_ok=True)
|
||||||
|
origen, copia = sqlite3.connect(inventario), sqlite3.connect(os.path.join(destino, "datos", INVENTARIO))
|
||||||
|
try:
|
||||||
|
origen.backup(copia)
|
||||||
|
finally:
|
||||||
|
copia.close()
|
||||||
|
origen.close()
|
||||||
|
except Exception:
|
||||||
|
shutil.rmtree(destino, ignore_errors=True)
|
||||||
|
raise
|
||||||
|
with open(os.path.join(base, MARCA_MUDANZA), "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(destino + "\n\nOrganiLAB se instaló en la carpeta de arriba el " + time.strftime("%d/%m/%Y")
|
||||||
|
+ ", con sus datos.\nEsta copia ya no se usa: si la abres, se abre la instalada.\n"
|
||||||
|
"Puedes borrar esta carpeta entera cuando quieras.\n")
|
||||||
|
return destino
|
||||||
|
|
||||||
|
|
||||||
|
class YaInstalado(Exception):
|
||||||
|
"""Ya hay un OrganiLAB instalado en este equipo, en otra carpeta."""
|
||||||
|
def __init__(self, otro):
|
||||||
|
super().__init__(f"ya hay un OrganiLAB instalado en {otro['base']} (versión {otro['version']})")
|
||||||
|
self.otro = otro
|
||||||
|
|
||||||
|
|
||||||
def _peso_kb(base):
|
def _peso_kb(base):
|
||||||
total = 0
|
total = 0
|
||||||
for raiz, dirs, files in os.walk(base):
|
for raiz, dirs, files in os.walk(base):
|
||||||
@@ -181,6 +294,18 @@ def instalar(base, version, solo_los_que_hay=False):
|
|||||||
return {"base": base, "accesos": hechos, "orden": f'"{exe}" {args}'.strip()}
|
return {"base": base, "accesos": hechos, "orden": f'"{exe}" {args}'.strip()}
|
||||||
|
|
||||||
|
|
||||||
|
def poner(base, version, avance=None):
|
||||||
|
"""Instalar de verdad: si el programa es un paquete que no está en su sitio definitivo, se copia allí; y
|
||||||
|
se da de alta en Windows desde donde quede. El resultado dice dónde quedó y si hubo mudanza."""
|
||||||
|
if not hay_que_mudar(base):
|
||||||
|
return {**instalar(base, version), "mudado": False}
|
||||||
|
otro = instalado()
|
||||||
|
if otro and not _misma(otro["base"], base) and os.path.isfile(os.path.join(otro["base"], "OrganizadorLabstream.exe")):
|
||||||
|
raise YaInstalado(otro)
|
||||||
|
destino = mudar(base, avance=avance)
|
||||||
|
return {**instalar(destino, version), "mudado": True}
|
||||||
|
|
||||||
|
|
||||||
def al_dia(base, version):
|
def al_dia(base, version):
|
||||||
"""Tras una actualización: si este programa está dado de alta, Windows tiene que enseñar la versión
|
"""Tras una actualización: si este programa está dado de alta, Windows tiene que enseñar la versión
|
||||||
nueva y los accesos directos tienen que apuntar a lo que ahora lo abre."""
|
nueva y los accesos directos tienen que apuntar a lo que ahora lo abre."""
|
||||||
@@ -222,7 +347,11 @@ def principal(que, base, version, silencioso=False):
|
|||||||
return V.IDOK if silencioso else u.MessageBoxW(None, texto, NOMBRE, botones)
|
return V.IDOK if silencioso else u.MessageBoxW(None, texto, NOMBRE, botones)
|
||||||
try:
|
try:
|
||||||
if que == "instalar":
|
if que == "instalar":
|
||||||
instalar(base, version)
|
r = poner(base, version)
|
||||||
|
if r["mudado"]:
|
||||||
|
decir(f"OrganiLAB quedó instalado en:\n\n{r['base']}\n\nÁbrelo desde el menú Inicio o el Escritorio. "
|
||||||
|
"La carpeta desde la que lo instalaste ya no hace falta: puedes borrarla.")
|
||||||
|
else:
|
||||||
decir("OrganiLAB ya está en el menú Inicio y en el Escritorio.\n\n"
|
decir("OrganiLAB ya está en el menú Inicio y en el Escritorio.\n\n"
|
||||||
"Para quitarlo: Configuración › Aplicaciones › Aplicaciones instaladas.")
|
"Para quitarlo: Configuración › Aplicaciones › Aplicaciones instaladas.")
|
||||||
return 0
|
return 0
|
||||||
@@ -233,6 +362,11 @@ def principal(que, base, version, silencioso=False):
|
|||||||
desinstalar(base)
|
desinstalar(base)
|
||||||
decir(f"Quitado. La carpeta del programa sigue en:\n\n{base}\n\nSi ya no la quieres, bórrala a mano.")
|
decir(f"Quitado. La carpeta del programa sigue en:\n\n{base}\n\nSi ya no la quieres, bórrala a mano.")
|
||||||
return 0
|
return 0
|
||||||
|
except YaInstalado as ex:
|
||||||
|
decir(f"Ya hay un OrganiLAB instalado en este equipo (versión {ex.otro['version']}), en:\n\n{ex.otro['base']}\n\n"
|
||||||
|
"Ábrelo desde el menú Inicio. Para ponerlo al día, usa «Buscar actualizaciones» dentro del programa.",
|
||||||
|
V.MB_OK | V.MB_ICONINFORMATION)
|
||||||
|
return 1
|
||||||
except Exception as ex: # noqa
|
except Exception as ex: # noqa
|
||||||
decir(f"No se pudo: {ex}", V.MB_OK | V.MB_ICONWARNING)
|
decir(f"No se pudo: {ex}", V.MB_OK | V.MB_ICONWARNING)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"nombre": "OrganiLAB",
|
"nombre": "OrganiLAB",
|
||||||
"version": "3.0.0",
|
"version": "3.1.0",
|
||||||
"builtAt": "2026-10-04T14:50:25.000Z",
|
"builtAt": "2026-10-04T17:23:53.000Z",
|
||||||
"archivo": "OrganizadorLabstream-portatil-3.0.0.zip",
|
"archivo": "OrganizadorLabstream-portatil-3.1.0.zip",
|
||||||
"size": 195378394,
|
"size": 195387012,
|
||||||
"sha256": "98c43f1f06865f233e98d4b4b8f2cae82e1386def7b38673ce5a4cd01bc850dd",
|
"sha256": "1c80a369bcdc8f07c5a9db1dec1bd95dcd390a8e65714ec60251388544ee6523",
|
||||||
"app": {
|
"app": {
|
||||||
"archivo": "OrganizadorLabstream-app-3.0.0.zip",
|
"archivo": "OrganizadorLabstream-app-3.1.0.zip",
|
||||||
"size": 11565815,
|
"size": 11574433,
|
||||||
"sha256": "09d9024a41370bdd07dbbd3fe1110822c0f51ce4987f62cf9dd185ad9aa3e82e",
|
"sha256": "cedd68ade3f61ccd2958e3578bf46be8b92e20d73aade3b7e455585e53898d3e",
|
||||||
"url": "/api/herramientas/OrganizadorLabstream-app-3.0.0.zip"
|
"url": "/api/herramientas/OrganizadorLabstream-app-3.1.0.zip"
|
||||||
},
|
},
|
||||||
"notes": "OrganiLAB pasa a ser un programa de Windows: ventana propia, se instala en el menú Inicio y el Escritorio, y el .exe vuelve a abrir con Smart App Control encendido.",
|
"notes": "El programa se instala solo en su sitio definitivo (tu carpeta de programas) y trae un manual dentro, en «Manual». Además, dos copias ya no pueden compartir puerto.",
|
||||||
"firma": {
|
"firma": {
|
||||||
"alg": "rsa2048-pkcs1v15-sha256",
|
"alg": "rsa2048-pkcs1v15-sha256",
|
||||||
"valor": "6456976cf21259a5ae91b5ba5cc8074b552cfec0a389f8b9c89c7af962841cdb523f8f39b7143281d7d965079ee0471fc8b7f14c79636d2cc443315131a3fc53ec6a22ff87b1c49d806e4a795c2365b83f8236a523a1349730b90300eff68cc84ad32f58e663798e77301024f870e094c1fa112b994d55f719c1cdf8c22b0375f6eb24276bfa9837d755ef07c71e9b701df073fd4c94cf3ddccc6e5a2203e48458398beb4f38d2d68ebab2810a0ac56add4b9294321cb4eb3083f1e2d717bd2b2a6b638cadb8140e48f4d46e3e0a13686b462477bb8e9a3923f7a178c5e326f65a8ae90ff68949ac6a5bd2cf5758a53dd79779c79b4f67414f0251aae3f9566e"
|
"valor": "1ca46a8c6af640d62da3b8a97500661ae2d1e5442b66fb9ba9ee5268e3cfd85c1c1b6c2949484742adbdc758788bb5df323654b6a8533dd1ce3a93e9b974b0870bfa7c2f74fec4f6ef922cf1228c2d290a09a54dd62cabe846cdb40ed79276cc36940283e39db57ccc5f392daaf074672546f38cc3342cbc569d76723d365c2807755d95e8f129fb5e309952da27f645ef6130af7abb3f1356f895a92745ae890ad0c5b9d2a2139bc85ea5b66bcd3b2385d74c2d4b2bc438acf474e8dde041951d0c8f9949b39bf214d38ce3f0a80451f9c1681a9e3e53c2d28abf3e570201cc624b248213392ada7c892743a26c2be40d5170149d72d2dd3cd8bd92e3bff5d7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ import ctypes
|
|||||||
import time
|
import time
|
||||||
import queue
|
import queue
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import stat as stat_mod
|
import stat as stat_mod
|
||||||
import hashlib
|
import hashlib
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -39,7 +40,7 @@ import actualizador
|
|||||||
asistente.O = sys.modules[__name__]
|
asistente.O = sys.modules[__name__]
|
||||||
actualizador.O = sys.modules[__name__]
|
actualizador.O = sys.modules[__name__]
|
||||||
|
|
||||||
VERSION = "3.0.0"
|
VERSION = "3.1.0"
|
||||||
|
|
||||||
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
|
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
|
||||||
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
|
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
|
||||||
@@ -3751,6 +3752,19 @@ def estado_carpeta(ruta):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ServidorLocal(ThreadingHTTPServer):
|
||||||
|
"""El servidor de la interfaz, con el puerto para él solo. ThreadingHTTPServer pide «reutilizar dirección»,
|
||||||
|
y en Windows eso significa que dos programas pueden escuchar a la vez en el mismo puerto y las peticiones
|
||||||
|
van a cualquiera de los dos: dos OrganiLAB con datos distintos (el instalado y una copia suelta, o el de
|
||||||
|
ensayo y el de verdad) se contestaban el uno por el otro sin que nada fallara a la vista."""
|
||||||
|
allow_reuse_address = False
|
||||||
|
|
||||||
|
def server_bind(self):
|
||||||
|
if hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
|
||||||
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
|
||||||
|
super().server_bind()
|
||||||
|
|
||||||
|
|
||||||
class Manejador(BaseHTTPRequestHandler):
|
class Manejador(BaseHTTPRequestHandler):
|
||||||
protocol_version = "HTTP/1.1"
|
protocol_version = "HTTP/1.1"
|
||||||
|
|
||||||
@@ -4132,7 +4146,7 @@ class Manejador(BaseHTTPRequestHandler):
|
|||||||
AYUDA = """OrganiLAB (Organizador Labstream) — uso por comando
|
AYUDA = """OrganiLAB (Organizador Labstream) — uso por comando
|
||||||
|
|
||||||
OrganizadorLabstream.exe abre OrganiLAB en su ventana
|
OrganizadorLabstream.exe abre OrganiLAB en su ventana
|
||||||
OrganizadorLabstream.exe instalar lo pone en el menú Inicio y en el Escritorio (desinstalar lo quita)
|
OrganizadorLabstream.exe instalar lo instala en su sitio: carpeta de programas, menú Inicio y Escritorio (desinstalar lo quita)
|
||||||
OrganizadorLabstream.exe "D:\\Clientes\\Carpeta" (o arrastra la carpeta encima del .exe) ASISTENTE: la escanea con barra
|
OrganizadorLabstream.exe "D:\\Clientes\\Carpeta" (o arrastra la carpeta encima del .exe) ASISTENTE: la escanea con barra
|
||||||
de progreso, da el diagnóstico por tipo de archivo y pregunta paso a paso qué hacer
|
de progreso, da el diagnóstico por tipo de archivo y pregunta paso a paso qué hacer
|
||||||
OrganizadorLabstream.exe asistente "ruta" [--si] [--simular] lo mismo por comando (--si acepta lo recomendado sin preguntar)
|
OrganizadorLabstream.exe asistente "ruta" [--si] [--simular] lo mismo por comando (--si acepta lo recomendado sin preguntar)
|
||||||
@@ -4359,6 +4373,7 @@ SOLTADOS = {"ts": 0, "rutas": []}
|
|||||||
SESION_PATH = os.path.join(DATOS, "sesion.json") # qué copia tiene abiertos estos datos, y en qué puerto atiende
|
SESION_PATH = os.path.join(DATOS, "sesion.json") # qué copia tiene abiertos estos datos, y en qué puerto atiende
|
||||||
_CANDADO = [] # el de «una sola copia abierta»; vive lo que el proceso
|
_CANDADO = [] # el de «una sola copia abierta»; vive lo que el proceso
|
||||||
SILENCIO_NAVEGADOR = 180 # segundos sin que la página pregunte para dar el navegador por cerrado
|
SILENCIO_NAVEGADOR = 180 # segundos sin que la página pregunte para dar el navegador por cerrado
|
||||||
|
RELEVO = [] # el .exe que toma el relevo al salir (tras instalarse en su sitio)
|
||||||
|
|
||||||
|
|
||||||
def _al_soltar(rutas):
|
def _al_soltar(rutas):
|
||||||
@@ -4471,7 +4486,8 @@ def _vigilar(v, fin):
|
|||||||
|
|
||||||
def _recien_abierta(v):
|
def _recien_abierta(v):
|
||||||
"""Con la interfaz ya a la vista: si el programa está dado de alta en Windows, pone al día su versión y
|
"""Con la interfaz ya a la vista: si el programa está dado de alta en Windows, pone al día su versión y
|
||||||
sus accesos directos; y la primera vez que se abre un paquete en un equipo, ofrece darlo de alta."""
|
sus accesos directos; y la primera vez que se abre un paquete en un equipo, ofrece instalarlo: copiarlo a
|
||||||
|
su sitio definitivo, darlo de alta y seguir desde allí."""
|
||||||
try:
|
try:
|
||||||
import ventana
|
import ventana
|
||||||
import instalar
|
import instalar
|
||||||
@@ -4480,14 +4496,35 @@ def _recien_abierta(v):
|
|||||||
oferta = CONFIG.get("instalacion") or {}
|
oferta = CONFIG.get("instalacion") or {}
|
||||||
if not CONGELADO or os.environ.get("ORGANIZADOR_PUERTO") or oferta.get("ofrecida") or instalar.instalado_aqui(BASE):
|
if not CONGELADO or os.environ.get("ORGANIZADOR_PUERTO") or oferta.get("ofrecida") or instalar.instalado_aqui(BASE):
|
||||||
return
|
return
|
||||||
|
if instalar.instalado() and not instalar.instalado_aqui(BASE):
|
||||||
|
return # ya hay otro instalado en este equipo: esta copia va por libre
|
||||||
CONFIG["instalacion"] = {**oferta, "ofrecida": True} # se pregunta una sola vez
|
CONFIG["instalacion"] = {**oferta, "ofrecida": True} # se pregunta una sola vez
|
||||||
guardar_json(CONFIG_PATH, CONFIG)
|
guardar_json(CONFIG_PATH, CONFIG)
|
||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
|
if not instalar.hay_que_mudar(BASE): # ya está en su sitio: solo falta darlo de alta
|
||||||
if v.mensaje("¿Quieres tener OrganiLAB en el menú Inicio y en el Escritorio, como un programa más de Windows?\n\n"
|
if v.mensaje("¿Quieres tener OrganiLAB en el menú Inicio y en el Escritorio, como un programa más de Windows?\n\n"
|
||||||
"No copia nada: el programa se queda en esta carpeta. Se quita desde Configuración › Aplicaciones.",
|
"Se quita desde Configuración › Aplicaciones.", ventana.MB_YESNO | ventana.MB_ICONQUESTION) == ventana.IDYES:
|
||||||
ventana.MB_YESNO | ventana.MB_ICONQUESTION) == ventana.IDYES:
|
|
||||||
instalar.instalar(BASE, VERSION)
|
instalar.instalar(BASE, VERSION)
|
||||||
v.mensaje("Listo. Búscalo como «OrganiLAB» en el menú Inicio, o ábrelo desde el Escritorio.")
|
v.mensaje("Listo. Búscalo como «OrganiLAB» en el menú Inicio, o ábrelo desde el Escritorio.")
|
||||||
|
return
|
||||||
|
if v.mensaje("¿Instalar OrganiLAB en este equipo?\n\nSe copia a tu carpeta de programas, queda en el menú Inicio y en el "
|
||||||
|
"Escritorio, y se quita desde Configuración › Aplicaciones. La carpeta desde la que lo has abierto ya no hará falta.",
|
||||||
|
ventana.MB_YESNO | ventana.MB_ICONQUESTION) != ventana.IDYES:
|
||||||
|
return
|
||||||
|
v.evaluar("modal(" + json.dumps("Instalando OrganiLAB…") + ", " + json.dumps(
|
||||||
|
'<p><span class="spin"></span> Copiando el programa a su sitio definitivo. Tarda unos segundos; al terminar se abre solo desde allí.</p>') + ", null)")
|
||||||
|
try:
|
||||||
|
r = instalar.poner(BASE, VERSION, avance=v.progreso)
|
||||||
|
except Exception as ex: # noqa
|
||||||
|
v.progreso(None)
|
||||||
|
v.evaluar("document.querySelector('#modal').classList.remove('on')")
|
||||||
|
v.mensaje(f"No se pudo instalar: {ex}\n\nEl programa sigue funcionando desde esta carpeta.", ventana.MB_OK | ventana.MB_ICONWARNING)
|
||||||
|
return
|
||||||
|
v.progreso(None)
|
||||||
|
v.mensaje("OrganiLAB quedó instalado.\n\nAhora se cierra y se abre desde su sitio. La carpeta desde la que lo abriste "
|
||||||
|
"ya no hace falta: puedes borrarla.")
|
||||||
|
RELEVO.append(os.path.join(r["base"], "OrganizadorLabstream.exe"))
|
||||||
|
v.cerrar()
|
||||||
except Exception as ex: # noqa
|
except Exception as ex: # noqa
|
||||||
print("no se pudo dar de alta en Windows:", ex)
|
print("no se pudo dar de alta en Windows:", ex)
|
||||||
|
|
||||||
@@ -4539,8 +4576,20 @@ def _atender_navegador():
|
|||||||
def main():
|
def main():
|
||||||
global PUERTO
|
global PUERTO
|
||||||
args_tmp = [a for a in sys.argv[1:] if not a.startswith("--")]
|
args_tmp = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||||
|
if CONGELADO and "--puente" not in sys.argv:
|
||||||
|
import instalar
|
||||||
|
instalada = instalar.mudado(BASE)
|
||||||
|
if instalada: # desde esta carpeta ya se instaló el programa: se abre el instalado
|
||||||
|
if sys.argv[1:]:
|
||||||
|
sys.exit(subprocess.call([instalada] + sys.argv[1:]))
|
||||||
|
subprocess.Popen([instalada], cwd=os.path.dirname(instalada), close_fds=True)
|
||||||
|
return
|
||||||
if args_tmp and args_tmp[0].lower() in ("instalar", "desinstalar") and "--puente" not in sys.argv:
|
if args_tmp and args_tmp[0].lower() in ("instalar", "desinstalar") and "--puente" not in sys.argv:
|
||||||
import instalar # hablan con cuadros de mensaje: no hace falta consola
|
import instalar # hablan con cuadros de mensaje: no hace falta consola
|
||||||
|
if args_tmp[0].lower() == "instalar" and instalar.hay_que_mudar(BASE) and not _soy_la_unica():
|
||||||
|
import ventana # la mudanza se lleva los datos: con el programa abierto se quedarían a medias
|
||||||
|
ctypes.windll.user32.MessageBoxW(None, "Cierra OrganiLAB antes de instalarlo.", "OrganiLAB", ventana.MB_OK | ventana.MB_ICONINFORMATION)
|
||||||
|
sys.exit(1)
|
||||||
sys.exit(instalar.principal(args_tmp[0].lower(), BASE, VERSION, silencioso="--si" in sys.argv))
|
sys.exit(instalar.principal(args_tmp[0].lower(), BASE, VERSION, silencioso="--si" in sys.argv))
|
||||||
if args_tmp and "--puente" not in sys.argv:
|
if args_tmp and "--puente" not in sys.argv:
|
||||||
_asegurar_consola()
|
_asegurar_consola()
|
||||||
@@ -4583,13 +4632,14 @@ def main():
|
|||||||
if huerfanas:
|
if huerfanas:
|
||||||
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Reanudar).")
|
print(f"{huerfanas} conversiones quedaron a medias la última vez: vuelven a la cola en espera (Reanudar).")
|
||||||
try:
|
try:
|
||||||
srv = ThreadingHTTPServer(("127.0.0.1", PUERTO), Manejador)
|
srv = ServidorLocal(("127.0.0.1", PUERTO), Manejador)
|
||||||
except OSError:
|
except OSError:
|
||||||
if os.environ.get("ORGANIZADOR_PUERTO"):
|
if os.environ.get("ORGANIZADOR_PUERTO"):
|
||||||
print(f"El puerto {PUERTO} está ocupado por otro programa.")
|
print(f"El puerto {PUERTO} está ocupado por otro programa.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
# el puerto de siempre lo tiene otro programa: se toma uno libre (a la ventana le da igual cuál sea)
|
# el puerto de siempre lo tiene otro programa (u otro OrganiLAB con otros datos): se toma uno libre,
|
||||||
srv = ThreadingHTTPServer(("127.0.0.1", 0), Manejador)
|
# que a la ventana le da igual cuál sea
|
||||||
|
srv = ServidorLocal(("127.0.0.1", 0), Manejador)
|
||||||
PUERTO = srv.server_address[1]
|
PUERTO = srv.server_address[1]
|
||||||
url = f"http://127.0.0.1:{PUERTO}/"
|
url = f"http://127.0.0.1:{PUERTO}/"
|
||||||
guardar_json(SESION_PATH, {"pid": os.getpid(), "puerto": PUERTO})
|
guardar_json(SESION_PATH, {"pid": os.getpid(), "puerto": PUERTO})
|
||||||
@@ -4620,6 +4670,9 @@ def main():
|
|||||||
while en_marcha(): # la ventana se fue con trabajo pendiente: se acaba antes de salir
|
while en_marcha(): # la ventana se fue con trabajo pendiente: se acaba antes de salir
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
srv.shutdown()
|
srv.shutdown()
|
||||||
|
srv.server_close()
|
||||||
|
if RELEVO: # se acaba de instalar en su sitio: sigue desde allí
|
||||||
|
subprocess.Popen([RELEVO[0]], cwd=os.path.dirname(RELEVO[0]), close_fds=True)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
os.remove(SESION_PATH)
|
os.remove(SESION_PATH)
|
||||||
|
|||||||
94
probar.py
94
probar.py
@@ -169,10 +169,11 @@ class Windows(unittest.TestCase):
|
|||||||
abrir la página de ese otro. Y este Windows no puede abrir la ventana (aquí, porque falta el cargador
|
abrir la página de ese otro. Y este Windows no puede abrir la ventana (aquí, porque falta el cargador
|
||||||
de WebView2): la interfaz va al navegador y el programa se va solo cuando esa página deja de preguntar."""
|
de WebView2): la interfaz va al navegador y el programa se va solo cuando esa página deja de preguntar."""
|
||||||
tmp = tempfile.mkdtemp(prefix="organilab-imprevistos-")
|
tmp = tempfile.mkdtemp(prefix="organilab-imprevistos-")
|
||||||
tapon = socket.socket()
|
# quien ocupa el puerto es un servidor como el de otro OrganiLAB: pide «reutilizar dirección», y en
|
||||||
tapon.bind(("127.0.0.1", 0))
|
# Windows eso dejaba que un segundo programa escuchara en el mismo puerto sin que nada fallara
|
||||||
tapon.listen(1)
|
import http.server
|
||||||
ocupado = tapon.getsockname()[1]
|
tapon = http.server.ThreadingHTTPServer(("127.0.0.1", 0), http.server.BaseHTTPRequestHandler)
|
||||||
|
ocupado = tapon.server_address[1]
|
||||||
try:
|
try:
|
||||||
for f in ("organizador.py", "reglas.py", "asistente.py", "actualizador.py", "resolve_bridge.py", "firma.py", "ventana.py", "instalar.py"):
|
for f in ("organizador.py", "reglas.py", "asistente.py", "actualizador.py", "resolve_bridge.py", "firma.py", "ventana.py", "instalar.py"):
|
||||||
shutil.copy(os.path.join(AQUI, f), tmp)
|
shutil.copy(os.path.join(AQUI, f), tmp)
|
||||||
@@ -192,7 +193,72 @@ class Windows(unittest.TestCase):
|
|||||||
self.assertTrue(p.stdout.strip().splitlines()[-1].startswith("FIN "))
|
self.assertTrue(p.stdout.strip().splitlines()[-1].startswith("FIN "))
|
||||||
self.assertFalse(os.path.exists(os.path.join(tmp, "datos", "sesion.json")))
|
self.assertFalse(os.path.exists(os.path.join(tmp, "datos", "sesion.json")))
|
||||||
finally:
|
finally:
|
||||||
tapon.close()
|
tapon.server_close()
|
||||||
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_instalarse_en_su_sitio(self):
|
||||||
|
"""Instalar un paquete lo lleva a su sitio definitivo con sus datos, sin borrar nada del origen. No viaja
|
||||||
|
lo que es de la sesión abierta ni lo que se rehace solo, y la copia de origen queda sabiendo adónde se fue."""
|
||||||
|
import sqlite3
|
||||||
|
import winreg
|
||||||
|
import instalar
|
||||||
|
tmp = tempfile.mkdtemp(prefix="organilab-mudanza-")
|
||||||
|
base, destino = os.path.join(tmp, "Descargas", "OrganizadorLabstream"), os.path.join(tmp, "Programs", "OrganiLAB")
|
||||||
|
for rel in ("OrganizadorLabstream.exe", "organizador.py", "static/index.html", "static/organilab.ico", "ffmpeg/ffmpeg.exe",
|
||||||
|
"datos/config.json", "datos/bitacora.jsonl", "datos/sesion.json", "datos/ventana/EBWebView/cache.bin",
|
||||||
|
"actualizacion/nuevo.zip", "__pycache__/organizador.cpython-312.pyc"):
|
||||||
|
p = os.path.join(base, rel)
|
||||||
|
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||||
|
with open(p, "wb") as fh:
|
||||||
|
fh.write(rel.encode() * 50)
|
||||||
|
con = sqlite3.connect(os.path.join(base, "datos", "inventario.sqlite"))
|
||||||
|
con.execute("CREATE TABLE archivos (ruta TEXT)")
|
||||||
|
con.execute("INSERT INTO archivos VALUES ('V:/Cliente/C0001.MP4')")
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
inicio, escritorio = os.path.join(tmp, "inicio", "OrganiLAB.lnk"), os.path.join(tmp, "escritorio", "OrganiLAB.lnk")
|
||||||
|
os.makedirs(os.path.dirname(inicio))
|
||||||
|
os.makedirs(os.path.dirname(escritorio))
|
||||||
|
viejos = instalar._accesos, instalar.CLAVE, instalar.sitio_definitivo
|
||||||
|
instalar._accesos, instalar.CLAVE, instalar.sitio_definitivo = (lambda: (inicio, escritorio)), r"Software\OrganiLAB-ensayo\Mudanza", (lambda: destino)
|
||||||
|
try:
|
||||||
|
self.assertTrue(instalar.hay_que_mudar(base))
|
||||||
|
self.assertEqual(instalar.mudado(base), "")
|
||||||
|
pasos = []
|
||||||
|
r = instalar.poner(base, "3.1.0", avance=pasos.append)
|
||||||
|
self.assertTrue(r["mudado"])
|
||||||
|
self.assertTrue(instalar._misma(r["base"], destino))
|
||||||
|
self.assertTrue(pasos and 0 < pasos[-1] <= 1)
|
||||||
|
for rel in ("OrganizadorLabstream.exe", "organizador.py", "static/index.html", "ffmpeg/ffmpeg.exe", "datos/config.json", "datos/bitacora.jsonl"):
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(destino, rel)), rel)
|
||||||
|
for rel in ("datos/sesion.json", "datos/ventana", "actualizacion", "__pycache__", instalar.MARCA_MUDANZA):
|
||||||
|
self.assertFalse(os.path.exists(os.path.join(destino, rel)), rel)
|
||||||
|
con = sqlite3.connect(os.path.join(destino, "datos", "inventario.sqlite"))
|
||||||
|
self.assertEqual(con.execute("SELECT ruta FROM archivos").fetchall(), [("V:/Cliente/C0001.MP4",)])
|
||||||
|
con.close()
|
||||||
|
# del origen no se ha ido nada, y ya sabe adónde se instaló
|
||||||
|
for rel in ("OrganizadorLabstream.exe", "ffmpeg/ffmpeg.exe", "datos/sesion.json", "datos/inventario.sqlite"):
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(base, rel)), rel)
|
||||||
|
self.assertTrue(instalar._misma(os.path.dirname(instalar.mudado(base)), destino))
|
||||||
|
# dado de alta desde su sitio, no desde Descargas
|
||||||
|
self.assertTrue(instalar.instalado_aqui(destino))
|
||||||
|
self.assertTrue(instalar._misma(instalar.leer_acceso(inicio)["exe"], os.path.join(destino, "OrganizadorLabstream.exe")))
|
||||||
|
# instalar otra vez desde el origen avisa de que ya hay uno; desde su sitio, es solo darlo de alta
|
||||||
|
with self.assertRaises(instalar.YaInstalado):
|
||||||
|
instalar.poner(base, "3.1.0")
|
||||||
|
self.assertFalse(instalar.poner(destino, "3.1.0")["mudado"])
|
||||||
|
# y un destino ocupado no se pisa nunca
|
||||||
|
instalar.desinstalar(destino)
|
||||||
|
with self.assertRaises(FileExistsError):
|
||||||
|
instalar.mudar(base)
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(destino, "organizador.py")))
|
||||||
|
finally:
|
||||||
|
for clave in (instalar.CLAVE, r"Software\OrganiLAB-ensayo"):
|
||||||
|
try:
|
||||||
|
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, clave)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
instalar._accesos, instalar.CLAVE, instalar.sitio_definitivo = viejos
|
||||||
shutil.rmtree(tmp, ignore_errors=True)
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
|
||||||
def test_orden_de_arranque(self):
|
def test_orden_de_arranque(self):
|
||||||
@@ -215,6 +281,24 @@ class Windows(unittest.TestCase):
|
|||||||
self.assertEqual(p.stdout.strip(), "sigue")
|
self.assertEqual(p.stdout.strip(), "sigue")
|
||||||
|
|
||||||
|
|
||||||
|
class Manual(unittest.TestCase):
|
||||||
|
def test_manual_al_dia(self):
|
||||||
|
"""El manual vive dentro de la interfaz, con un apartado por cada pestaña del programa: si se añade una
|
||||||
|
vista y no se documenta, esto falla."""
|
||||||
|
import re
|
||||||
|
with open(os.path.join(AQUI, "static", "index.html"), encoding="utf-8") as fh:
|
||||||
|
html = fh.read()
|
||||||
|
vistas = re.findall(r'<a data-t="([a-z]+)"[^>]*>([^<]+)</a>', html[:html.index("</nav>")])
|
||||||
|
manual = html[html.index('<section id="t-manual">'):html.index("<!-- ============ AJUSTES")]
|
||||||
|
apartados = re.findall(r"<h3>([^<]+)</h3>", manual)
|
||||||
|
self.assertGreaterEqual(len(vistas), 8)
|
||||||
|
for clave, etiqueta in vistas:
|
||||||
|
if clave != "manual":
|
||||||
|
self.assertIn(etiqueta.replace("⚙", "").strip(), apartados)
|
||||||
|
self.assertGreater(manual.count("<dt>"), 35)
|
||||||
|
self.assertEqual(manual.count("<dt>"), manual.count("<dd>"))
|
||||||
|
|
||||||
|
|
||||||
class Firma(unittest.TestCase):
|
class Firma(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ sin abrirlo; esto abre OrganiLAB sobre una copia temporal (programa y datos apar
|
|||||||
comprueba desde otro hilo lo que solo se puede comprobar con la ventana delante:
|
comprueba desde otro hilo lo que solo se puede comprobar con la ventana delante:
|
||||||
|
|
||||||
py -3.12 prueba_ventana.py el programa desde el código
|
py -3.12 prueba_ventana.py el programa desde el código
|
||||||
py -3.12 prueba_ventana.py --paquete dist\OrganizadorLabstream el paquete, arrancado por su .exe
|
py -3.12 prueba_ventana.py --paquete dist\OrganizadorLabstream el paquete, arrancado por su .exe, y su instalación
|
||||||
… --captura ruta.png deja una imagen de la ventana entera
|
… --captura ruta.png deja una imagen de la ventana entera
|
||||||
|
|
||||||
Durante medio minuto se ve una ventana que se abre, se mueve sola y se cierra. No hay que tocar nada.
|
Durante medio minuto se ve una ventana que se abre, se mueve sola y se cierra. No hay que tocar nada.
|
||||||
@@ -138,6 +138,21 @@ def marca_de(datos):
|
|||||||
return (zlib.crc32(os.path.normcase(os.path.abspath(datos)).encode("utf-8")) & 0x7FFFFFFF) or 1
|
return (zlib.crc32(os.path.normcase(os.path.abspath(datos)).encode("utf-8")) & 0x7FFFFFFF) or 1
|
||||||
|
|
||||||
|
|
||||||
|
def cerrar_las_de(V, *marcas):
|
||||||
|
"""Cierra las copias de ensayo que sigan abiertas (con sus cuadros de diálogo), pase lo que pase en la
|
||||||
|
prueba: una copia olvidada se queda en la pantalla y, peor, atendiendo."""
|
||||||
|
for _ in range(3):
|
||||||
|
abiertas = [h for m in marcas for h in V._ventanas(m)]
|
||||||
|
if not abiertas:
|
||||||
|
return
|
||||||
|
for h in abiertas:
|
||||||
|
d = dialogo_de(h)
|
||||||
|
if d:
|
||||||
|
U.PostMessageW(d, WM_CLOSE, 0, 0)
|
||||||
|
U.PostMessageW(h, WM_CLOSE, 0, 0)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
def captura_pedida():
|
def captura_pedida():
|
||||||
return sys.argv[sys.argv.index("--captura") + 1] if "--captura" in sys.argv else ""
|
return sys.argv[sys.argv.index("--captura") + 1] if "--captura" in sys.argv else ""
|
||||||
|
|
||||||
@@ -206,6 +221,10 @@ def desde_el_codigo(tmp):
|
|||||||
return r
|
return r
|
||||||
aj = v.en_ui(ajustes)
|
aj = v.en_ui(ajustes)
|
||||||
anota("lo que es de navegador está apagado", not any(aj.values()), ", ".join(k for k, x in aj.items() if x) or "todo apagado")
|
anota("lo que es de navegador está apagado", not any(aj.values()), ", ".join(k for k, x in aj.items() if x) or "todo apagado")
|
||||||
|
v.evaluar("irA('manual')")
|
||||||
|
entradas = v.evaluar("document.querySelector('#t-manual').classList.contains('on') ? document.querySelectorAll('#t-manual dt').length : 0")
|
||||||
|
anota("el manual está dentro del programa", (entradas or 0) > 35, f"{entradas} entradas")
|
||||||
|
v.evaluar("irA('carpeta')")
|
||||||
anota("la interfaz sabe que está en la ventana", v.evaluar("document.documentElement.classList.contains('nativo')") is True)
|
anota("la interfaz sabe que está en la ventana", v.evaluar("document.documentElement.classList.contains('nativo')") is True)
|
||||||
|
|
||||||
# el avance en la barra de tareas
|
# el avance en la barra de tareas
|
||||||
@@ -348,10 +367,89 @@ def el_paquete(tmp, origen):
|
|||||||
proc.kill()
|
proc.kill()
|
||||||
anota("al cerrar la ventana se cierra el programa", proc.returncode == 0, f"código {proc.returncode}")
|
anota("al cerrar la ventana se cierra el programa", proc.returncode == 0, f"código {proc.returncode}")
|
||||||
anota("y deja de atender", estado() is None)
|
anota("y deja de atender", estado() is None)
|
||||||
|
cerrar_las_de(V, marca)
|
||||||
p = subprocess.run([exe, "ayuda"], capture_output=True, timeout=60, stdin=subprocess.DEVNULL, env=entorno)
|
p = subprocess.run([exe, "ayuda"], capture_output=True, timeout=60, stdin=subprocess.DEVNULL, env=entorno)
|
||||||
anota("por consola responde a sus comandos", p.returncode == 0 and b"asistente" in p.stdout)
|
anota("por consola responde a sus comandos", p.returncode == 0 and b"asistente" in p.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def instalar_el_paquete(tmp, origen):
|
||||||
|
"""La instalación de punta a punta, sin tocar el Windows de verdad: con ORGANILAB_ENSAYO el «equipo» es una
|
||||||
|
carpeta temporal. El paquete se abre desde unas «Descargas», ofrece instalarse, se copia a su sitio, se da
|
||||||
|
de alta, se cierra y vuelve a abrirse desde allí; y abrir después la copia de Descargas abre la instalada."""
|
||||||
|
import winreg
|
||||||
|
equipo = os.path.join(tmp, "equipo")
|
||||||
|
os.environ["ORGANILAB_ENSAYO"] = equipo
|
||||||
|
os.environ.pop("ORGANIZADOR_PUERTO", None)
|
||||||
|
sys.path.insert(0, AQUI)
|
||||||
|
import ventana as V
|
||||||
|
import instalar
|
||||||
|
descarga = os.path.join(tmp, "Descargas", "OrganizadorLabstream")
|
||||||
|
shutil.copytree(origen, descarga, ignore=shutil.ignore_patterns("ffmpeg", "actualizacion", "__pycache__"))
|
||||||
|
os.makedirs(os.path.join(descarga, "ffmpeg"))
|
||||||
|
with open(os.path.join(descarga, "ffmpeg", "LICENSE.txt"), "w") as fh:
|
||||||
|
fh.write("en el paquete completo aquí va ffmpeg")
|
||||||
|
destino = instalar.sitio_definitivo()
|
||||||
|
exe_d, exe_i = os.path.join(descarga, "OrganizadorLabstream.exe"), os.path.join(destino, "OrganizadorLabstream.exe")
|
||||||
|
marca_d, marca_i = marca_de(os.path.join(descarga, "datos")), marca_de(os.path.join(destino, "datos"))
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen([exe_d], cwd=tmp)
|
||||||
|
hs = espera(lambda: V._ventanas(marca_d), 40)
|
||||||
|
if not hs:
|
||||||
|
anota("el paquete abre desde Descargas", False)
|
||||||
|
proc.kill()
|
||||||
|
return
|
||||||
|
h = hs[0]
|
||||||
|
d = espera(lambda: dialogo_de(h), 25)
|
||||||
|
pregunta = " ".join(texto(x) for x in hijos(d)) if d else ""
|
||||||
|
anota("la primera vez ofrece instalarlo en el equipo", "Instalar OrganiLAB en este equipo" in pregunta, pregunta[8:70])
|
||||||
|
if d:
|
||||||
|
U.PostMessageW(d, WM_COMMAND, 6, 0) # «Sí»
|
||||||
|
|
||||||
|
def aviso_final():
|
||||||
|
x = dialogo_de(h)
|
||||||
|
return x if x and "quedó instalado" in " ".join(texto(y) for y in hijos(x)) else None
|
||||||
|
d = espera(aviso_final, 90)
|
||||||
|
anota("se copia a su sitio y lo dice", bool(d))
|
||||||
|
if d:
|
||||||
|
U.PostMessageW(d, WM_CLOSE, 0, 0) # solo tiene «Aceptar»
|
||||||
|
hi = espera(lambda: V._ventanas(marca_i), 60)
|
||||||
|
anota("se cierra y vuelve a abrirse solo desde su sitio", bool(hi) and espera(lambda: not V._ventanas(marca_d), 30) is True)
|
||||||
|
try:
|
||||||
|
proc.wait(30)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
anota("en su sitio están el programa, sus datos y su ffmpeg",
|
||||||
|
all(os.path.exists(os.path.join(destino, x)) for x in ("OrganizadorLabstream.exe", "runtime", "static", "datos\\config.json", "ffmpeg\\LICENSE.txt")))
|
||||||
|
a = instalar.leer_acceso(instalar._accesos()[0]) if os.path.exists(instalar._accesos()[0]) else None
|
||||||
|
anota("queda dado de alta desde su sitio (Inicio, Escritorio y Aplicaciones)",
|
||||||
|
instalar.instalado_aqui(destino) and bool(a) and instalar._misma(a["exe"], exe_i) and os.path.exists(instalar._accesos()[1]),
|
||||||
|
a["exe"] if a else "sin acceso directo")
|
||||||
|
anota("del origen no se borra nada, y queda la nota de la mudanza", os.path.exists(exe_d) and bool(instalar.mudado(descarga)))
|
||||||
|
if hi:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(destino, "datos", "sesion.json"), encoding="utf-8") as fh:
|
||||||
|
puerto = json.load(fh)["puerto"]
|
||||||
|
e = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{puerto}/api/estado", timeout=5).read())
|
||||||
|
except Exception: # noqa
|
||||||
|
e = {}
|
||||||
|
anota("el instalado atiende con sus datos", e.get("congelado") is True and (e.get("config") or {}).get("instalacion", {}).get("ofrecida") is True,
|
||||||
|
f"versión {e.get('version')}")
|
||||||
|
subprocess.run([exe_d], cwd=tmp, timeout=40)
|
||||||
|
time.sleep(2.5)
|
||||||
|
anota("abrir la copia de Descargas abre la instalada", not V._ventanas(marca_d) and len(V._ventanas(marca_i)) == 1)
|
||||||
|
U.PostMessageW(hi[0], WM_CLOSE, 0, 0)
|
||||||
|
anota("y se cierra como siempre", espera(lambda: not V._ventanas(marca_i), 25) is True)
|
||||||
|
time.sleep(2)
|
||||||
|
finally:
|
||||||
|
cerrar_las_de(V, marca_d, marca_i)
|
||||||
|
for clave in (instalar.CLAVE, "Software\\OrganiLAB-ensayo"):
|
||||||
|
try:
|
||||||
|
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, clave)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
os.environ.pop("ORGANILAB_ENSAYO", None)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
guardia = threading.Timer(300, lambda: os._exit(3)) # pase lo que pase, no se queda colgada
|
guardia = threading.Timer(300, lambda: os._exit(3)) # pase lo que pase, no se queda colgada
|
||||||
guardia.daemon = True
|
guardia.daemon = True
|
||||||
@@ -360,6 +458,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
if "--paquete" in sys.argv:
|
if "--paquete" in sys.argv:
|
||||||
el_paquete(tmp, os.path.abspath(sys.argv[sys.argv.index("--paquete") + 1]))
|
el_paquete(tmp, os.path.abspath(sys.argv[sys.argv.index("--paquete") + 1]))
|
||||||
|
instalar_el_paquete(tmp, os.path.abspath(sys.argv[sys.argv.index("--paquete") + 1]))
|
||||||
else:
|
else:
|
||||||
desde_el_codigo(tmp)
|
desde_el_codigo(tmp)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -68,6 +68,15 @@
|
|||||||
nav a{padding:13px 11px;color:var(--mut);text-decoration:none;border-bottom:2px solid transparent;cursor:pointer;font-weight:600}
|
nav a{padding:13px 11px;color:var(--mut);text-decoration:none;border-bottom:2px solid transparent;cursor:pointer;font-weight:600}
|
||||||
nav a.on{color:var(--tx);border-color:var(--tx)}
|
nav a.on{color:var(--tx);border-color:var(--tx)}
|
||||||
nav a.eng{margin-left:auto;font-size:12.5px;font-weight:500}
|
nav a.eng{margin-left:auto;font-size:12.5px;font-weight:500}
|
||||||
|
nav a.eng2{font-size:12.5px;font-weight:500}
|
||||||
|
/* ── manual ── */
|
||||||
|
.manual{max-width:1120px}
|
||||||
|
.manual h3{margin:24px 0 8px;font-size:14px;padding-bottom:6px;border-bottom:1px solid var(--line)}
|
||||||
|
.manual dl{display:grid;grid-template-columns:230px 1fr;gap:10px 22px;margin:0}
|
||||||
|
.manual dt{font-weight:600}
|
||||||
|
.manual dd{margin:0}
|
||||||
|
.manual dd i{font-style:normal;color:var(--mut)}
|
||||||
|
@media (max-width:900px){.manual dl{grid-template-columns:1fr;gap:2px 0}.manual dd{margin-bottom:10px}}
|
||||||
main{padding:22px 28px 40px;max-width:1720px;overflow-x:hidden}
|
main{padding:22px 28px 40px;max-width:1720px;overflow-x:hidden}
|
||||||
section{display:none}section.on{display:block}
|
section{display:none}section.on{display:block}
|
||||||
h2.v{margin:0 0 2px;font-size:18px;font-weight:700;letter-spacing:-.01em}
|
h2.v{margin:0 0 2px;font-size:18px;font-weight:700;letter-spacing:-.01em}
|
||||||
@@ -347,6 +356,7 @@
|
|||||||
<a data-t="respaldar" tabindex="0">Respaldar</a>
|
<a data-t="respaldar" tabindex="0">Respaldar</a>
|
||||||
<a data-t="historial" tabindex="0">Historial</a>
|
<a data-t="historial" tabindex="0">Historial</a>
|
||||||
<a data-t="ajustes" class="eng" tabindex="0" title="Clientes, sus carpetas y plantillas">⚙ Ajustes</a>
|
<a data-t="ajustes" class="eng" tabindex="0" title="Clientes, sus carpetas y plantillas">⚙ Ajustes</a>
|
||||||
|
<a data-t="manual" class="eng2" tabindex="0" title="Qué hace cada cosa y cómo lo hace">Manual</a>
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
@@ -537,6 +547,104 @@
|
|||||||
<div id="historial"></div>
|
<div id="historial"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ============ MANUAL ============ -->
|
||||||
|
<section id="t-manual">
|
||||||
|
<div class="cab"><h2 class="v">Manual</h2></div>
|
||||||
|
<p class="ayuda">Cada función del programa: qué hace y, en gris, cómo lo hace. El orden es el de las pestañas de arriba.</p>
|
||||||
|
<div class="manual">
|
||||||
|
|
||||||
|
<h3>Para empezar</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Instalarlo</dt><dd>La primera vez que se abre pregunta si lo instala en el equipo: queda en el menú Inicio, en el Escritorio y en Configuración › Aplicaciones. <i>Cómo: se copia con sus datos a tu carpeta de programas y se da de alta desde ahí; no pide permisos de administrador y de la carpeta original no borra nada.</i></dd>
|
||||||
|
<dt>Añadir la carpeta de un cliente</dt><dd>Suéltala en la ventana, pulsa «Elegir carpeta…» o pega su ruta. Pregunta el nombre del cliente y qué clase de trabajo le haces, y te enseña cómo va a quedar ordenada. <i>Cómo: al confirmar solo la lee; no mueve nada hasta que tú lo apliques.</i></dd>
|
||||||
|
<dt>Clase de trabajo</dt><dd>Piezas mes a mes, anuncios y campañas, un trabajo puntual o material de Labstream. <i>Cómo: cada clase tiene su plantilla de carpetas, y es lo único que decide dónde se guarda cada cosa.</i></dd>
|
||||||
|
<dt>Tus carpetas</dt><dd>Al abrir no se entra en ninguna: la portada lista las que ya conoce, cada una con su último escaneo, para entrar con un clic.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Carpeta</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Escanear</dt><dd>Lee nombres, tamaños y fechas de todo lo que hay dentro. <i>Cómo: recorre la carpeta sin abrir los archivos, guarda un inventario y clasifica cada cosa por su extensión, su nombre y la carpeta donde está: cámara, proyecto, exportado, stock, proxy, basura…</i></dd>
|
||||||
|
<dt>Lista de acciones</dt><dd>Todo lo que se puede hacer ahora en esa carpeta, en orden, cada cosa con su cifra y su botón; lo ya hecho sale tachado. <i>Cómo: son consultas al inventario, por eso es instantánea.</i></dd>
|
||||||
|
<dt>Hacer las N de una vez</dt><dd>Ejecuta seguidas las acciones que no necesitan que elijas nada: apartar basura y proxies, ordenar por fecha, ordenar lo demás, apartar stock viejo y juntar repetidos. <i>Cómo: lo dudoso no entra nunca; convertir, respaldar y borrar carpetas vacías tienen su propio botón.</i></dd>
|
||||||
|
<dt>Quitar de la lista</dt><dd>La carpeta deja de aparecer en el programa y, si quieres, se olvida su inventario. En el disco no se toca nada.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Revisar y ordenar</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Qué hay</dt><dd>Qué pesa y de qué clase es, por cámara, por mes y por tipo de archivo, con avisos de lo raro: material suelto, meses mal escritos, dos sistemas de orden a la vez.</dd>
|
||||||
|
<dt>Por nombrar</dt><dd>Las carpetas cuyo nombre no reconoce. Propone qué son y tú lo confirmas. <i>Cómo: mira qué tienen dentro; lo confirmado se guarda en un diccionario y vale para todas las carpetas y discos.</i></dd>
|
||||||
|
<dt>Qué va a pasar</dt><dd>El plan: a dónde iría cada cosa, sin mover nada. Va en tres pasos que se aplican por separado: apartar basura y proxies, ordenar por año y mes (y por día de rodaje si ese mes hubo más de uno), y ordenar lo demás (proyectos por programa, exportados, música…). <i>Cómo: usa la plantilla de la clase de trabajo y la fecha de cada archivo, la más antigua entre creación y modificación; un render hereda la fecha de su original. Cada fila va marcada como segura, por revisar o dudosa.</i></dd>
|
||||||
|
<dt>Aplicar</dt><dd>Hace los movimientos marcados, siempre dentro de la carpeta del cliente. <i>Cómo: no pisa nada; si en el destino ya hay un archivo con ese nombre, lo deja donde está y lo avisa. Cada movimiento queda apuntado para poder deshacerlo.</i></dd>
|
||||||
|
<dt>Stock viejo</dt><dd>Aparta a <span class="path">_Para borrar\stock</span> las tomas de Envato de piezas de más de 1, 2, 3, 6 o 12 meses, que se vuelven a descargar con la suscripción. <i>Cómo: las reconoce por el nombre (terminan en «-utc») o por estar en una carpeta «Elements».</i></dd>
|
||||||
|
<dt>Repetidos</dt><dd>Cuando el mismo archivo está en varios sitios, deja uno y junta los demás en una subcarpeta <span class="path">Duplicados</span>. <i>Cómo: mismo nombre y mismo peso, y antes de apartar compara el contenido (principio, centro y final); si difiere, no lo toca.</i></dd>
|
||||||
|
<dt>Carpetas vacías</dt><dd>Lista los huecos que deja ordenar y los borra. Es lo único que el programa borra, porque dentro no hay nada. <i>Cómo: cada una se comprueba en el disco justo antes; se puede deshacer, y no entra en «hacer de una vez».</i></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Mapa</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>El árbol</dt><dd>Cómo quedan las carpetas con cada clase de trabajo y qué va en cada una, con el color de lo que guarda. <i>Cómo: se dibuja con las plantillas de verdad, así que enseña exactamente lo que el programa va a hacer.</i></dd>
|
||||||
|
<dt>Tocar una carpeta</dt><dd>Dice qué guarda y cuánto material de eso hay hoy, y deja cambiar ahí mismo a dónde va. «Sin carpeta asignada» lista lo que todavía no tiene sitio.</dd>
|
||||||
|
<dt>Cambiar un nombre</dt><dd>Renombra una carpeta en todas las plantillas que pasan por ella y, si quieres, también las que ya existen en el disco. <i>Cómo: es un movimiento normal, queda en el historial y se deshace.</i></dd>
|
||||||
|
<dt>¿Dónde va a parar este archivo?</dt><dd>Escribes el nombre de un archivo, exista o no, y dice qué es, de qué cámara o programa viene y en qué carpeta lo dejaría. <i>Cómo: con el mismo clasificador que usa el escaneo.</i></dd>
|
||||||
|
<dt>Colores en Windows</dt><dd>Pinta las carpetas en el Explorador con el color de lo que guardan, y los quita con un clic. <i>Cómo: Windows no tiene color de carpeta, así que les pone un icono de color; hay discos que no lo permiten, y entonces lo dice y no deja nada escrito.</i></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Convertidor</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Buscar clips</dt><dd>Encuentra los clips de cámara viejos y pesados (de fábrica, de más de 3 meses y 300 MB) y calcula cuánto pesarían convertidos. <i>Cómo: lee códec, duración y resolución con ffprobe; se salta lo que ya es ligero y los exportados.</i></dd>
|
||||||
|
<dt>Formato y calidad</dt><dd>H.265 10 bits (el recomendado, unas diez veces más ligero), H.265 ligero, H.264 o ProRes proxy, LT y HQ; calidad de ahorro, equilibrada o máxima. «Motores» te dice qué acelera tu tarjeta gráfica.</dd>
|
||||||
|
<dt>Convertir</dt><dd>Mete los clips en una cola que va uno tras otro en segundo plano. Se puede detener, reanudar, reintentar y vaciar. <i>Cómo: con ffmpeg, por tarjeta gráfica si la hay y si no por procesador; los BRAW pasan por DaVinci Resolve Studio, que tiene que estar abierto en un proyecto.</i></dd>
|
||||||
|
<dt>Verificar</dt><dd>Cada clip se comprueba solo al terminar: duración, códec y los 10 bits. Lo que no cuadra no se da por bueno.</dd>
|
||||||
|
<dt>Reemplazar</dt><dd>El convertido ocupa el sitio del original, con su misma fecha, y el original se aparta a <span class="path">_Para borrar\originales</span>.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Respaldar</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Calcular</dt><dd>Dice cuánto ocuparía la copia, por tipo de medio, y qué originales de cámara quedarían fuera por no estar convertidos.</dd>
|
||||||
|
<dt>Copiar</dt><dd>Copia a otro disco una versión ligera: misma estructura, los clips en su versión convertida, sin stock, proxies ni basura. Todo, o solo lo de más de 3, 6 o 12 meses. <i>Cómo: es incremental; lo que ya está igual se salta, así que se puede detener y retomar. Deja <span class="path">INDICE.csv</span> y <span class="path">_MAPA.md</span> para encontrar cosas sin montar el disco.</i></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Historial</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Deshacer</dt><dd>Cada vez que el programa mueve algo queda aquí como un lote, y «Deshacer» devuelve cada archivo a donde estaba. <i>Cómo: en orden inverso; si alguien movió un archivo después, ese se salta y se avisa.</i></dd>
|
||||||
|
<dt>_Para borrar</dt><dd>Enseña lo que hay apartado. El programa no lo borra nunca: lo revisa y lo borra una persona desde el Explorador.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Ajustes</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Clientes</dt><dd>Nombre, clase de trabajo, sus carpetas y los otros nombres con que aparece en los discos.</dd>
|
||||||
|
<dt>Actualizaciones</dt><dd>«Buscar actualizaciones» mira si hay versión nueva; el interruptor de «consultar al arrancar» viene apagado. <i>Cómo: descarga el paquete de Labstream OS, comprueba su firma y su huella, se cierra, se sustituye sin tocar tus datos y vuelve a abrirse.</i></dd>
|
||||||
|
<dt>Plantillas</dt><dd>En «Avanzado»: la ruta exacta de cada tipo de material en cada clase de trabajo. Es lo mismo que se cambia desde el Mapa.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>La ventana</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>En curso</dt><dd>La barra de abajo enseña el avance de lo que esté corriendo (escaneo, lectura de clips, conversión, copia) con su botón de detener; el mismo avance sale en el botón de la barra de tareas de Windows, que parpadea al terminar algo largo.</dd>
|
||||||
|
<dt>Cerrar con trabajo en marcha</dt><dd>Avisa de lo que está haciendo; si cierras, sigue hasta terminar y se cierra solo. Abrirlo otra vez devuelve la misma ventana.</dd>
|
||||||
|
<dt>Una sola copia</dt><dd>Abrirlo dos veces trae al frente la ventana que ya había. <i>Cómo: dos copias sobre los mismos datos se pisarían el inventario.</i></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Sin abrir el programa</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Arrastrar una carpeta</dt><dd>Sobre el .exe o sobre «Organizar carpeta (arrastra aqui).bat»: la escanea, da el diagnóstico y pregunta paso a paso en una consola.</dd>
|
||||||
|
<dt>Por comando</dt><dd><span class="path">OrganizadorLabstream.exe ayuda</span> lista los comandos: asistente, organizar, escanear, stock, respaldo, duplicados, deshacer, actualizar, instalar y desinstalar.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Lo que nunca hace</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>Borrar archivos</dt><dd>Lo que sobra va a <span class="path">_Para borrar</span>, dentro de la misma carpeta del cliente, y lo repetido a <span class="path">Duplicados</span>.</dd>
|
||||||
|
<dt>Pisar o sacar de su carpeta</dt><dd>No sobrescribe nada y no mueve nada fuera de la carpeta del cliente.</dd>
|
||||||
|
<dt>Salir a la red</dt><dd>Todo ocurre en este equipo. Solo se conecta cuando le pides buscar una versión nueva.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h3>Dónde guarda lo suyo</h3>
|
||||||
|
<dl>
|
||||||
|
<dt>La carpeta datos</dt><dd>Dentro de la carpeta del programa: el inventario, el historial, los ajustes y lo que ha aprendido. Si cambias de equipo, llévate esa carpeta.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ============ AJUSTES ============ -->
|
<!-- ============ AJUSTES ============ -->
|
||||||
<section id="t-ajustes">
|
<section id="t-ajustes">
|
||||||
<div class="cab"><h2 class="v">Clientes</h2><div class="der"><input type="text" id="filtroCli" placeholder="buscar cliente…" style="min-width:200px"><button id="btnAddCliente">+ Añadir cliente</button></div></div>
|
<div class="cab"><h2 class="v">Clientes</h2><div class="der"><input type="text" id="filtroCli" placeholder="buscar cliente…" style="min-width:200px"><button id="btnAddCliente">+ Añadir cliente</button></div></div>
|
||||||
@@ -628,7 +736,7 @@ $$('#tabsRev a').forEach(a => a.onclick = () => subRev(a.dataset.s));
|
|||||||
const cargarVista = seguro(async t => {
|
const cargarVista = seguro(async t => {
|
||||||
t = t || vistaActual();
|
t = t || vistaActual();
|
||||||
const r = cfgRaiz();
|
const r = cfgRaiz();
|
||||||
if (t !== 'carpeta' && t !== 'ajustes' && r) { try { CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raizActual())); pintarIndice(); } catch (e) { console.warn(e); } }
|
if (t !== 'carpeta' && t !== 'ajustes' && t !== 'manual' && r) { try { CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raizActual())); pintarIndice(); } catch (e) { console.warn(e); } }
|
||||||
if (t === 'carpeta') await cargarCarpeta();
|
if (t === 'carpeta') await cargarCarpeta();
|
||||||
if (t === 'revisar') { await cargarMapa(); await cargarVoc(); if (PLAN) pintarPlan(); if (STOCK.length && STOCK[0].raiz !== raizActual()) { STOCK = []; $('#stockCaja').style.display = 'none'; } if (VACIAS && VACIAS.raiz !== raizActual()) { VACIAS = null; pintarVacias(); } }
|
if (t === 'revisar') { await cargarMapa(); await cargarVoc(); if (PLAN) pintarPlan(); if (STOCK.length && STOCK[0].raiz !== raizActual()) { STOCK = []; $('#stockCaja').style.display = 'none'; } if (VACIAS && VACIAS.raiz !== raizActual()) { VACIAS = null; pintarVacias(); } }
|
||||||
if (t === 'mapa') await cargarMapaEst();
|
if (t === 'mapa') await cargarMapaEst();
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user