# -*- coding: utf-8 -*- r""" Construye la versión portátil y los paquetes de actualización. Uso: py -3.12 empaquetar.py [ruta\a\ffmpeg\bin] Resultado: dist\OrganizadorLabstream\ carpeta completa (con ffmpeg) OrganizadorLabstream-portatil-.zip primera instalación (con ffmpeg, ~190 MB) OrganizadorLabstream-app-.zip actualización (sin ffmpeg ni datos, ~15 MB) ..\labstream-os\public\herramientas\organizador.json manifiesto para el OS (versión, tamaños, sha256) Después: subir los dos zips al NAS (storage/herramientas/) y desplegar el OS. """ import os import re import sys import glob import json import shutil import hashlib import zipfile import datetime import subprocess AQUI = os.path.dirname(os.path.abspath(__file__)) os.chdir(AQUI) VERSION = re.search(r'^VERSION = "([^"]+)"', open("organizador.py", encoding="utf-8").read(), re.M).group(1) print("versión", VERSION) ffbin = sys.argv[1] if len(sys.argv) > 1 else "" if not ffbin: for c in glob.glob(os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WinGet\Packages\Gyan.FFmpeg*\ffmpeg-*\bin")): ffbin = c if not ffbin and shutil.which("ffmpeg"): ffbin = os.path.dirname(shutil.which("ffmpeg")) print("ffmpeg desde:", ffbin or "(no encontrado: el zip irá sin ffmpeg)") for d in ("build", "dist"): shutil.rmtree(d, ignore_errors=True) subprocess.check_call([sys.executable, "-m", "PyInstaller", "--noconfirm", "--clean", "--onedir", "--windowed", "--name", "OrganizadorLabstream", "--icon", os.path.join("static", "organilab.ico"), "--hidden-import", "resolve_bridge", "--hidden-import", "reglas", "--hidden-import", "asistente", "--hidden-import", "actualizador", "--hidden-import", "firma", "--collect-all", "webview", "--collect-all", "clr_loader", "--collect-all", "pythonnet", "--exclude-module", "tkinter", "organizador.py"]) dest = os.path.join("dist", "OrganizadorLabstream") shutil.copytree("static", os.path.join(dest, "static")) os.makedirs(os.path.join(dest, "datos"), exist_ok=True) for f in ("config.json", "diccionario.json", "plantillas.json"): src = os.path.join("datos", f) if os.path.exists(src): shutil.copy(src, os.path.join(dest, "datos", f)) # la config del paquete sale limpia: sin el recorrido ya visto ni raíces de prueba cfg_p = os.path.join(dest, "datos", "config.json") if os.path.exists(cfg_p): cfg = json.load(open(cfg_p, encoding="utf-8")) cfg.pop("tour_visto", None) cfg["raices"] = [] # el paquete sale SIN carpetas: cada equipo añade las suyas y se escanean al añadirlas json.dump(cfg, open(cfg_p, "w", encoding="utf-8"), ensure_ascii=False, indent=2) shutil.copy("README.md", dest) BAT = ('@echo off\r\nchcp 65001 >nul\r\ncd /d "%~dp0"\r\nif "%~1"=="" (\r\n echo Arrastra una carpeta de cliente encima de este archivo.\r\n pause\r\n exit /b\r\n)\r\n' '"%~dp0OrganizadorLabstream.exe" {cmd} "%~1"\r\n') with open(os.path.join(dest, "Organizar carpeta (arrastra aqui).bat"), "w", encoding="cp1252") as fh: fh.write(BAT.format(cmd="asistente")) with open(os.path.join(dest, "Apartar stock Envato viejo (arrastra aqui).bat"), "w", encoding="cp1252") as fh: fh.write(BAT.format(cmd="stock")) with open(os.path.join(dest, "LEEME.txt"), "w", encoding="utf-8") as fh: fh.write(f"OrganiLAB · Organizador Labstream {VERSION} (portátil)\n\n" "ORDENAR UNA CARPETA: arrastra la carpeta del cliente encima de «Organizar carpeta (arrastra aqui).bat»\n" "(o del propio .exe). Se escanea con barra de progreso, sale el diagnóstico y el asistente pregunta paso a paso.\n" "Por consola: OrganizadorLabstream.exe asistente \"D:\\Clientes\\Carpeta\" [--simular] [--si]\n" "Más comandos: OrganizadorLabstream.exe ayuda\n\n" "1. Doble clic en OrganizadorLabstream.exe abre OrganiLAB en su propia ventana (sin navegador).\n" "2. No hace falta instalar nada: Python y ffmpeg van dentro.\n3. Para convertir BRAW hace falta DaVinci Resolve Studio abierto en un proyecto.\n" "4. La app NUNCA borra: lo que sobra va a _Para borrar y lo repetido a subcarpetas Duplicados, para que una persona lo revise.\n" "5. Todo lo que aprende y la configuración queda en la carpeta datos\\ (llévala contigo si cambias de PC).\n" "6. Se actualiza solo desde Labstream OS (os.labstreamsas.com): avisa cuando hay versión nueva.\n" "Si Windows SmartScreen avisa, «Más información» → «Ejecutar de todas formas».\n") # ── paquete APP (actualización): todo menos ffmpeg y datos ── app_zip = f"OrganizadorLabstream-app-{VERSION}.zip" with zipfile.ZipFile(app_zip, "w", zipfile.ZIP_DEFLATED) as z: for raiz, dirs, files in os.walk(dest): rel_raiz = os.path.relpath(raiz, dest) primero = rel_raiz.split(os.sep)[0] if primero in ("ffmpeg", "datos"): continue for f in files: p = os.path.join(raiz, f) z.write(p, "OrganizadorLabstream/" + os.path.relpath(p, dest).replace(os.sep, "/")) # ── paquete COMPLETO (primera instalación): con ffmpeg ── if ffbin: os.makedirs(os.path.join(dest, "ffmpeg"), exist_ok=True) for exe in ("ffmpeg.exe", "ffprobe.exe"): shutil.copy(os.path.join(ffbin, exe), os.path.join(dest, "ffmpeg", exe)) lic = os.path.join(os.path.dirname(ffbin), "LICENSE") if os.path.exists(lic): shutil.copy(lic, os.path.join(dest, "ffmpeg", "LICENSE.txt")) for viejo in glob.glob("OrganizadorLabstream-portatil*.zip") + [z for z in glob.glob("OrganizadorLabstream-app-*.zip") if z != app_zip]: os.remove(viejo) full_zip = f"OrganizadorLabstream-portatil-{VERSION}.zip" shutil.make_archive(full_zip[:-4], "zip", "dist", "OrganizadorLabstream") def sha(p): h = hashlib.sha256() with open(p, "rb") as fh: for b in iter(lambda: fh.read(1 << 20), b""): h.update(b) return h.hexdigest() notas = os.environ.get("NOTAS", "") manifiesto = { "nombre": "OrganiLAB", "version": VERSION, "builtAt": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"), "archivo": full_zip, "size": os.path.getsize(full_zip), "sha256": sha(full_zip), "app": {"archivo": app_zip, "size": os.path.getsize(app_zip), "sha256": sha(app_zip), "url": f"/api/herramientas/{app_zip}"}, "notes": notas, } import firma if os.environ.get("FIRMA") == "no": print("AVISO: manifiesto sin firmar (FIRMA=no); los .exe 1.6.2+ no lo aceptarán") else: if not os.path.exists(firma.RUTA_PRIVADA): sys.exit(f"falta la clave de firma en {firma.RUTA_PRIVADA}: corre py -3.12 firma.py generar (o FIRMA=no para saltarlo)") _priv = firma.cargar_privada() manifiesto["firma"] = {"alg": "rsa2048-pkcs1v15-sha256", "valor": firma.firmar(firma.texto_manifiesto(manifiesto), _priv)} import actualizador if not firma.verificar(firma.texto_manifiesto(manifiesto), manifiesto["firma"]["valor"], actualizador.CLAVE_PUBLICA): sys.exit("la clave privada no corresponde a CLAVE_PUBLICA de actualizador.py; el manifiesto no serviría") print("manifiesto firmado") ruta_os = os.path.join(os.path.dirname(AQUI), "labstream-os", "public", "herramientas", "organizador.json") if os.path.isdir(os.path.dirname(ruta_os)): previo = {} try: previo = json.load(open(ruta_os, encoding="utf-8")) except Exception: # noqa pass if not notas: manifiesto["notes"] = previo.get("notes", "") if previo.get("version") == VERSION else "" json.dump(manifiesto, open(ruta_os, "w", encoding="utf-8"), ensure_ascii=False, indent=2) print("manifiesto escrito en", ruta_os) json.dump(manifiesto, open("manifiesto.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2) print(f"listo: {full_zip} ({os.path.getsize(full_zip) / 1e6:.0f} MB) · {app_zip} ({os.path.getsize(app_zip) / 1e6:.0f} MB)") print("Sube los dos zips a storage/herramientas/ del NAS y despliega el OS.")