From 9087afc4e335c46f3a2fd15fde15ece2e195a44d Mon Sep 17 00:00:00 2001 From: JesterDev <105895386+jstrdev@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:43:29 +0100 Subject: [PATCH] 1.22.1: permiso de red local en Mac --- LEEME.txt | 26 +++ README_EN.txt | 23 +++ app.py | 361 ++++++++++++++++++++++++++++++++++++- installer/macos/Info.plist | 1 + web/index.html | 152 ++++++++++++---- 5 files changed, 525 insertions(+), 38 deletions(-) diff --git a/LEEME.txt b/LEEME.txt index b63c466..4224a1a 100644 --- a/LEEME.txt +++ b/LEEME.txt @@ -323,6 +323,32 @@ Desinstalar. Tus ajustes se conservan por si vuelves a instalarlo. del cortafuegos de Windows. +NOVEDADES 1.22.1 +- Mac: la app pide el permiso de «Red local» de macOS. Sin él no puede + hablar con la PS5 aunque esté encendida; ahora, si macOS la bloquea, + lo dice y explica dónde activarlo (Ajustes del Sistema > Privacidad y + seguridad > Red local). + +NOVEDADES 1.22.0 +- Capturas: botón «Convertir a MP4». Marca los vídeos y los deja en MP4 + (H.264) en Descargas > PS5 Directo > Capturas, listos para Telegram, + WhatsApp o cualquier web. Si se grabaron en HDR, los pasa a colores + normales para que no se vean lavados. Usa FFmpeg: en Windows se instala + solo la primera vez (33 MB, pide permiso antes); en Linux y Mac usa el + del sistema (sudo apt install ffmpeg / brew install ffmpeg). +- Espacio: nueva sección «Limpiar restos». Busca envíos y bajadas que se + quedaron a medias, descompresiones cortadas y carpetas temporales, en la + consola y en el PC, con lo que ocupa cada cosa. Lo que todavía usa una + transferencia de la cola sale bloqueado y las carpetas dudosas salen + sin marcar. No se borra nada sin confirmar. + +NOVEDADES 1.21.2 +- «Bajar al PC» en la Biblioteca: la barra de arriba ahora dice a qué + velocidad baja y cuánto queda. +- La carátula del título que se está bajando se ve apagada y se va + llenando de color de abajo arriba, del 0 al 100 %, con el porcentaje. +- Cada carátula de la Biblioteca lleva un chip pequeño con lo que ocupa. + NOVEDADES 1.21.1 - Biblioteca: botón «Bajar al PC» en cada título (carpeta o imagen), con su progreso ahí mismo. Llega a la carpeta de PS5 Directo en el PC diff --git a/README_EN.txt b/README_EN.txt index c44ebae..b03878a 100644 --- a/README_EN.txt +++ b/README_EN.txt @@ -338,6 +338,29 @@ settings are kept in case you install it again. WHAT'S NEW ---------- +1.22.1 - Mac: the app asks for macOS "Local Network" permission. Without + it, it can't talk to the PS5 even when it's on; now, if macOS + blocks it, it says so and explains where to turn it on (System + Settings > Privacy & Security > Local Network). + +1.22.0 - Captures: "Convert to MP4" button. Tick the videos and they're + saved as MP4 (H.264) in Downloads > PS5 Directo > Capturas, ready + for Telegram, WhatsApp or any website. If they were recorded in + HDR, they get normal colours so they don't look washed out. Uses + FFmpeg: on Windows it's installed the first time only (33 MB, it + asks first); on Linux and Mac it uses the system one. + - Space: new "Clean up leftovers" section. Finds half-finished + sends and downloads, cut-off extractions and temporary folders on + the console and on the PC, with the size of each. Anything a + queued transfer still uses is locked and doubtful folders come + unticked. Nothing is deleted without confirmation. + +1.21.2 - Library "Download to PC": the bar at the top now shows the speed + and the time left. + - The cover of the title being downloaded turns dim and fills with + colour from bottom to top, 0 to 100 %, with the percentage. + - Each Library cover has a small chip with its size. + 1.21.1 - Library: "Download to PC" button on each title (folder or image), with its progress right there. It goes to the PS5 Directo folder on the PC (by default Downloads > PS5 Directo > Biblioteca, you diff --git a/app.py b/app.py index bf0298c..c0306ec 100644 --- a/app.py +++ b/app.py @@ -24,7 +24,7 @@ sys.path.insert(0, HERE) import engine as E # noqa: E402 import features as F # noqa: E402 -VERSION = "1.21.1" +VERSION = "1.22.1" # Repositorio de GitHub donde se publican las versiones (usuario/nombre). Si lo rellenas, # todas las copias buscan actualizaciones ahi sin tener que configurar nada. DEFAULT_REPO = "jstrdev/ps5directo" @@ -1607,6 +1607,31 @@ def fm_api(p, b): return {"roots": roots} if p == "/api/media/list": return {"items": media_list()} + if p == "/api/media/tools": + return {"ffmpeg": bool(ffmpeg_path()), "can_install": os.name == "nt", + "hint": "" if os.name == "nt" else ffmpeg_hint()} + if p == "/api/media/convert": + paths = [fm_clean(str(x)) for x in (b.get("paths") or [])] + paths = [x for x in paths if x.startswith(MEDIA_ROOT + "/") and re.search(r"(?i)\.(webm|mp4|mkv|mov)$", x)] + if not paths: + raise E.PipeError("Elige algún vídeo") + media_convert(paths) + return {} + if p == "/api/media/convstate": + with LOCK: + return {"conv": json.loads(json.dumps(CONV))} + if p == "/api/media/convcancel": + CONV_CANCEL.set() + return {} + if p == "/api/media/opendir": + d = MEDIA_CONV_DIR() + os.makedirs(d, exist_ok=True) + _open_quiet(d) + return {"dir": d} + if p == "/api/residue/scan": + return residue_scan() + if p == "/api/residue/delete": + return residue_delete([str(x) for x in (b.get("ids") or [])]) if p == "/api/media/clearcache": import shutil shutil.rmtree(MEDIA_DIR, ignore_errors=True) @@ -1932,10 +1957,31 @@ _CONN_RX = re.compile(r"(?i)10054|10053|10060|10061|10065|connection (reset|refu r"no respondi|errno (32|54|60|61|64|65|104|110|111|113)") +MAC_LAN_MSG = ("macOS no deja a PS5 Directo usar la red local. Ve a Ajustes del Sistema → Privacidad y seguridad → Red local, activa «PS5 Directo» (o «python3») y vuelve a abrir el programa.") + + +def mac_lan_blocked(host=None): + """En macOS 15+ hace falta el permiso «Red local». Si no lo tiene, conectar con la consola falla al + instante con "No route to host" (errno 65), aunque la PS5 este encendida.""" + if sys.platform != "darwin": + return False + host = E.clean_host(host or cfg_snapshot().get("ps5_ip") or "")[0] + if not host: + return False + t0 = time.time() + try: + socket.create_connection((host, 2121), timeout=2).close() + return False + except OSError as e: + return getattr(e, "errno", None) == 65 and time.time() - t0 < 0.5 + + def conn_error_text(e): """Los errores de red de Windows ("[WinError 10054] Se ha forzado la interrupción...") dicen poco: se cambian por un mensaje claro sobre la conexión con la consola.""" t = str(e) or e.__class__.__name__ + if sys.platform == "darwin" and re.search(r"(?i)no route to host|errno 65", t): + return MAC_LAN_MSG + " (%s)" % t if isinstance(e, (ConnectionError, TimeoutError, socket.timeout)) or _CONN_RX.search(t): return ("No hay conexión con la consola. Comprueba que la PS5 está encendida, que el FTP está cargado " "(arriba el punto «FTP» tiene que estar en verde) y pulsa Actualizar. (%s)" % t) @@ -5199,6 +5245,7 @@ def console_status(): "autoload": bool(slot and slot.get("slot") in autoload), "lost": LOST[p["id"]]}) out["reachable"] = bool(ver or out["loader"] or any(i["ok"] for i in out["items"]) or _port_open(cfg["ps5_ip"], 2121, 1) or _port_open(cfg["ps5_ip"], 5905, 1)) + out["mac_lan"] = MAC_LAN_MSG if (not out["reachable"] and mac_lan_blocked(cfg["ps5_ip"])) else "" return out @@ -5502,6 +5549,316 @@ def cleanup_delete(paths): return done +# --------------------------------------------------------------------------- +# Limpiar restos (menu Espacio): envios a medias y archivos temporales que ocupan sitio +# en la consola y en el PC. Solo se puede borrar lo que salio en la ultima busqueda. +# --------------------------------------------------------------------------- + +RESIDUE_RX = re.compile(r"(?i)\.(ps5part|ava-part|part|tmp|convirtiendo\.mp4)$") +RESIDUE_LAST = {} # id -> elemento de la ultima busqueda + + +def _local_size(path): + if os.path.isfile(path): + return os.path.getsize(path) + n = 0 + for r, _ds, fs in os.walk(path): + for f in fs: + try: + n += os.path.getsize(os.path.join(r, f)) + except OSError: + pass + return n + + +def residue_scan(): + cfg = cfg_snapshot() + items, warn = [], [] + with LOCK: + pending = [j.get("title") or "transferencia" for j in JOBS if j["status"] not in ("hecho", "cancelado")] + fm_busy = FMOP.get("busy") + + def add(where, path, label, size, checked=True, in_use=None, note=""): + items.append({"id": "%s:%s" % (where, path), "where": where, "path": path, "label": label, + "size": int(size or 0), "checked": checked and not in_use, "in_use": in_use, "note": note}) + + # --- en el PC + pc_dirs = [(pc_library_dir(), "Bajado de la Biblioteca"), (os.path.join(downloads_dir(), "PS5 Directo"), "Descargas"), + (MEDIA_CONV_DIR(), "Capturas")] + seen = set() + for d, lab in pc_dirs: + if not os.path.isdir(d) or d in seen: + continue + seen.add(d) + for r, ds, fs in os.walk(d): + if r.count(os.sep) - d.count(os.sep) > 3: + ds[:] = [] + for f in fs: + fp = os.path.join(r, f) + if RESIDUE_RX.search(f) and os.path.normcase(fp) not in seen: + seen.add(os.path.normcase(fp)) + add("pc", fp, "Bajada a medias (%s): %s" % (lab, f), _local_size(fp), + in_use="algo se está bajando ahora" if fm_busy else None) + tmp = pc_temp_root(cfg) + if os.path.isdir(tmp): + for n in sorted(os.listdir(tmp)): + fp = os.path.join(tmp, n) + add("pc", fp, "Temporal de transferencias por el PC: " + n, _local_size(fp), + in_use=("puede que lo use «%s» de la cola" % pending[0]) if pending else None) + if os.path.isdir(PCX_DIR): + for n in os.listdir(PCX_DIR): + fp = os.path.join(PCX_DIR, n) + if os.path.isdir(fp): + add("pc", fp, "Descompresión a medias en el PC: " + n, _local_size(fp), + in_use=("puede que lo use «%s» de la cola" % pending[0]) if pending else None) + if os.path.isdir(MEDIA_DIR): + sz = _local_size(MEDIA_DIR) + if sz: + add("pc", MEDIA_DIR, "Copias guardadas para ver las capturas (se vuelven a bajar al abrirlas)", sz) + + # --- en la consola + try: + bf = fm_bf_opt() + if bf is not None: + for it in cleanup_scan(): + add("ps5", it["path"], it["label"], it["size"], + in_use=("lo usa «%s» de la cola" % it["in_use"]) if it.get("in_use") else None) + else: + warn.append("BFpilot no responde: en la consola solo miro los archivos a medias, no la carpeta de trabajo.") + for root in ("/data/homebrew", "/data/pkg", "/data/etaHEN/games", "/mnt/usb0/homebrew", "/mnt/ext0/homebrew"): + try: + ents = fm_list(root)["entries"] + except Exception: + continue + for e in ents: + path = root + "/" + e["name"] + if not e["dir"] and RESIDUE_RX.search(e["name"]): + add("ps5", path, "Envío a medias: " + e["name"], e.get("size"), + in_use=("puede que lo use «%s» de la cola" % pending[0]) if pending else None) + elif e["dir"] and root.endswith("homebrew") and bf is not None and not bf.exists(path + "/sce_sys/param.json"): + add("ps5", path, "Carpeta sin sce_sys (¿envío cortado?): " + e["name"], bf.du(path) or 0, + checked=False, note="No es una aplicación completa. Márcala solo si sabes que es un envío que se cortó.") + except Exception as e: + warn.append("No pude revisar la consola: %s" % conn_error_text(e)) + items = [x for x in items if x["size"] or x["where"] == "ps5"] + items.sort(key=lambda x: (x["where"] != "ps5", -x["size"])) + with LOCK: + RESIDUE_LAST.clear() + RESIDUE_LAST.update({x["id"]: x for x in items}) + return {"items": items, "warn": warn} + + +def residue_delete(ids): + import shutil + with LOCK: + sel = [RESIDUE_LAST[i] for i in ids if i in RESIDUE_LAST] + freed, errs = 0, [] + con = [x for x in sel if x["where"] == "ps5"] + bf = fm_bf_opt() if con else None + for x in sel: + try: + if x["where"] == "pc": + if os.path.isdir(x["path"]): + shutil.rmtree(x["path"]) + elif os.path.exists(x["path"]): + os.remove(x["path"]) + elif x["path"].startswith(WORK_ROOT + "/"): + cleanup_delete([x["path"]]) + elif bf is not None: + bf.delete(x["path"]) + else: + ftp_rmtree(cfg_snapshot(), x["path"], threading.Event()) + freed += x["size"] + with LOCK: + RESIDUE_LAST.pop(x["id"], None) + except Exception as e: + errs.append("%s: %s" % (x["label"], conn_error_text(e))) + return {"freed": freed, "errors": errs} + + +# --------------------------------------------------------------------------- +# Convertir capturas para compartir: videos a MP4 (H.264) y, si estan en HDR, a SDR para que no +# se vean lavados. Usa FFmpeg: en Windows se descarga solo la primera vez (33 MB). +# --------------------------------------------------------------------------- + +FFMPEG_URL = "https://github.com/GyanD/codexffmpeg/releases/download/8.1/ffmpeg-8.1-essentials_build.7z" +FFMPEG_LOCK = threading.Lock() +CONV = {"busy": False, "phase": "", "i": 0, "n": 0, "pct": 0, "current": "", "done": [], "errors": [], "dir": ""} +CONV_CANCEL = threading.Event() + + +def MEDIA_CONV_DIR(): + return os.path.join(downloads_dir(), "PS5 Directo", "Capturas") + + +def ffmpeg_path(): + import shutil + mine = os.path.join(DATA_DIR, "tools", "ffmpeg", "ffmpeg.exe" if os.name == "nt" else "ffmpeg") + if os.path.isfile(mine): + return mine + w = shutil.which("ffmpeg") + if w: + return w + for c in ("/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"): + if os.name != "nt" and os.path.isfile(c): + return c + return None + + +def ffmpeg_hint(): + if sys.platform == "darwin": + return "Para convertir hace falta FFmpeg. Instálalo con Homebrew (brew.sh): brew install ffmpeg" + return "Para convertir hace falta FFmpeg. Instálalo (por ejemplo: sudo apt install ffmpeg) y vuelve a probar." + + +def ffmpeg_install(): + """Solo Windows: baja FFmpeg (compilacion de gyan.dev publicada en GitHub) y guarda ffmpeg.exe.""" + import subprocess + mine = os.path.join(DATA_DIR, "tools", "ffmpeg", "ffmpeg.exe") + with FFMPEG_LOCK: + if os.path.isfile(mine): + return mine + d = os.path.dirname(mine) + os.makedirs(d, exist_ok=True) + tmp = os.path.join(d, "ffmpeg.7z.part") + total = 33541925 + + def prog(got): + with LOCK: + CONV.update(pct=min(99, int(got * 100 / total))) + if CONV_CANCEL.is_set(): + raise E.Cancelled() + with LOCK: + CONV.update(phase="Instalando el convertidor (FFmpeg, 33 MB, solo la primera vez)", pct=0) + try: + F.http_get(FFMPEG_URL, limit=200 * 1024 * 1024, timeout=120, dest=tmp, progress=prog) + exe = E.find_7z(cfg_snapshot().get("sevenzip_path") or "") + if not exe: + raise E.PipeError("falta 7-Zip para abrir el paquete de FFmpeg") + r = subprocess.run([exe, "e", "-y", "-o" + d, tmp, "ffmpeg.exe", "-r"], capture_output=True, + timeout=300, **({"creationflags": 0x08000000} if os.name == "nt" else {})) + if r.returncode != 0 or not os.path.isfile(mine): + raise E.PipeError("no pude sacar ffmpeg.exe del paquete") + except E.Cancelled: + raise + except Exception as e: + raise E.PipeError("No pude instalar FFmpeg (%s). Puedes bajarlo de gyan.dev/ffmpeg y copiar ffmpeg.exe en %s" + % (conn_error_text(e), d)) + finally: + try: + os.remove(tmp) + except OSError: + pass + return mine + + +def _ff_probe(ff, src): + import subprocess + r = subprocess.run([ff, "-hide_banner", "-nostdin", "-i", src], capture_output=True, timeout=60, + **({"creationflags": 0x08000000} if os.name == "nt" else {})) + txt = r.stderr.decode("utf-8", "replace") + m = re.search(r"Duration:\s*(\d+):(\d+):([\d.]+)", txt) + dur = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + float(m.group(3)) if m else 0 + vline = next((ln for ln in txt.splitlines() if "Video:" in ln), "") + hdr = bool(re.search(r"smpte2084|arib-std-b67|bt2020", vline)) + h = re.search(r"\b\d{2,5}x(\d{2,5})\b", vline) + return dur, hdr, int(h.group(1)) if h else 0, "Audio:" in txt + + +def _ff_convert(ff, src, dst, dur, hdr, height, audio): + import subprocess + vf = [] + if height > 1080: + vf.append("scale=-2:1080") + if hdr: + vf.append("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0," + "zscale=t=bt709:m=bt709:r=tv") + vf.append("format=yuv420p") + cmd = [ff, "-hide_banner", "-nostdin", "-y", "-i", src, "-map", "0:v:0", "-map", "0:a:0?", "-vf", ",".join(vf), + "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-color_primaries", "bt709", + "-color_trc", "bt709", "-colorspace", "bt709"] + cmd += ["-c:a", "aac", "-b:a", "192k"] if audio else [] + cmd += ["-movflags", "+faststart", "-f", "mp4", "-progress", "pipe:1", "-nostats", dst] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + **({"creationflags": 0x08000000} if os.name == "nt" else {})) + err = [] + threading.Thread(target=lambda: err.extend(p.stderr.read().decode("utf-8", "replace").splitlines()[-8:]), + daemon=True).start() + for ln in p.stdout: + if CONV_CANCEL.is_set(): + p.kill() + break + m = re.match(rb"out_time_(?:us|ms)=(\d+)", ln) + if m and dur: + with LOCK: + CONV["pct"] = max(0, min(99, int(int(m.group(1)) / 1e6 * 100 / dur))) + rc = p.wait() + if CONV_CANCEL.is_set(): + raise E.Cancelled() + if rc != 0: + raise E.PipeError("FFmpeg falló: " + (" ".join(err[-2:]) or "código %d" % rc)) + + +def media_convert(paths): + with LOCK: + if CONV["busy"]: + raise E.PipeError("Ya se está convirtiendo") + CONV.update(busy=True, phase="", i=0, n=len(paths), pct=0, current="", done=[], errors=[], + dir=MEDIA_CONV_DIR()) + CONV_CANCEL.clear() + + def run(): + try: + ff = ffmpeg_path() + if not ff: + if os.name != "nt": + raise E.PipeError(ffmpeg_hint()) + ff = ffmpeg_install() + out_dir = MEDIA_CONV_DIR() + os.makedirs(out_dir, exist_ok=True) + for k, path in enumerate(paths, 1): + name = posixpath.basename(path) + stem = os.path.splitext(name)[0] + with LOCK: + CONV.update(i=k, current=name, pct=0, phase="Bajando de la consola") + try: + src, _ = media_cached(path) + dst = os.path.join(out_dir, stem + ".mp4") + with LOCK: + CONV.update(phase="Convirtiendo a MP4") + dur, hdr, height, audio = _ff_probe(ff, src) + if hdr: + with LOCK: + CONV.update(phase="Convirtiendo a MP4 y quitando el HDR (tarda un poco más)") + tmp = os.path.join(out_dir, stem + ".convirtiendo.mp4") + _ff_convert(ff, src, tmp, dur, hdr, height, audio) + os.replace(tmp, dst) + with LOCK: + CONV["done"].append(os.path.basename(dst)) + except E.Cancelled: + try: + os.remove(os.path.join(out_dir, stem + ".convirtiendo.mp4")) + except OSError: + pass + raise + except Exception as e: + with LOCK: + CONV["errors"].append("%s: %s" % (name, conn_error_text(e))) + with LOCK: + CONV.update(pct=100, phase="Hecho") + except E.Cancelled: + with LOCK: + CONV.update(phase="Cancelado") + except Exception as e: + with LOCK: + CONV["errors"].append(conn_error_text(e)) + CONV.update(phase="Error") + finally: + with LOCK: + CONV["busy"] = False + threading.Thread(target=run, daemon=True).start() + + # --------------------------------------------------------------------------- # Test de velocidad: descarga sola, envio solo y los dos a la vez # --------------------------------------------------------------------------- @@ -6283,7 +6640,7 @@ class Handler(BaseHTTPRequestHandler): except Exception: bases = ["/data/homebrew"] return self._send(200, {"ok": True, "dests": [b for b in bases if b.endswith("homebrew")]}) - if p.startswith("/api/fm/") or p.startswith("/api/lib/") or p.startswith("/api/space/") or p.startswith("/api/notifs") or p.startswith("/api/gu/") or p.startswith("/api/bk/") or p.startswith("/api/col/") or p.startswith("/api/media/") or p.startswith("/api/report") or p in ("/api/pick", "/api/history", "/api/history/clear", "/api/history/redo"): + if p.startswith("/api/fm/") or p.startswith("/api/lib/") or p.startswith("/api/space/") or p.startswith("/api/notifs") or p.startswith("/api/gu/") or p.startswith("/api/bk/") or p.startswith("/api/col/") or p.startswith("/api/media/") or p.startswith("/api/residue/") or p.startswith("/api/report") or p in ("/api/pick", "/api/history", "/api/history/clear", "/api/history/redo"): try: return self._send(200, dict(ok=True, **fm_api(p, b))) except E.PipeError as e: diff --git a/installer/macos/Info.plist b/installer/macos/Info.plist index 0615c03..53849f8 100644 --- a/installer/macos/Info.plist +++ b/installer/macos/Info.plist @@ -13,6 +13,7 @@ CFBundleInfoDictionaryVersion6.0 LSMinimumSystemVersion11.0 NSHighResolutionCapable + NSLocalNetworkUsageDescriptionPS5 Directo se conecta a tu PS5 por la red local (FTP, BFpilot y Payload Manager). / PS5 Directo connects to your PS5 over the local network (FTP, BFpilot and Payload Manager). LSApplicationCategoryTypepublic.app-category.utilities diff --git a/web/index.html b/web/index.html index af89b00..88cda1e 100644 --- a/web/index.html +++ b/web/index.html @@ -338,6 +338,14 @@ tr:hover .dots{opacity:1} .collib .mono{word-break:break-all} #libDl{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);margin:8px 0;font-size:13px} #libDl .bar{flex:1 1 160px;min-width:120px} +#mdConvBox{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);margin:8px 0;font-size:13px} +#mdConvBox .bar{flex:1 1 160px;min-width:120px}#mdConvBox .err{flex-basis:100%} +.rsd-g{font-weight:600;margin:12px 0 4px;font-size:13px} +.rsd-row{display:flex;gap:10px;align-items:flex-start;padding:6px 4px;margin:0;border-bottom:1px solid var(--line);font-size:13px;color:var(--text)} +.rsd-row input{width:auto;flex:none;margin-top:3px} +.rsd-row>div{flex:1;min-width:0;overflow-wrap:anywhere}.rsd-row .num{flex:none;font-variant-numeric:tabular-nums;font-weight:600} +.rsd-row.off{opacity:.6} +.rsd-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px} .colroot{display:inline-flex;align-items:center;gap:6px;padding:4px 6px 4px 11px;border-radius:999px;border:1px solid var(--line);font-size:13px;background:var(--panel)} .colroot button{border:0;background:none;color:var(--muted);cursor:pointer;font-size:14px;padding:0 4px} .lcrow{display:flex;gap:9px;align-items:flex-start;padding:7px 0;border-bottom:1px solid var(--line);font-size:13.5px} @@ -365,6 +373,10 @@ tr:hover .dots{opacity:1} .gupd{display:block;width:fit-content;margin:2px 0 6px;font-size:12.5px;color:#8fb6ff;text-decoration:none;border-bottom:1px dashed rgba(143,182,255,.45)} .gupd:hover{color:#b8d0ff;border-bottom-style:solid} .gcov img{width:100%;height:100%;object-fit:cover;display:block} +.gcov .sizeb{position:absolute;right:7px;bottom:7px;font-size:11px;font-weight:700;padding:2px 7px;border-radius:7px;background:rgba(0,0,0,.68);color:#e8eef8;letter-spacing:.02em} +.gcov .dlveil{position:absolute;left:0;right:0;top:0;height:calc(100% - var(--p,0%));backdrop-filter:grayscale(1) brightness(.42);-webkit-backdrop-filter:grayscale(1) brightness(.42);transition:height .8s ease;pointer-events:none} +.gcov .dlpct{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font:800 26px/1 system-ui,sans-serif;color:#fff;text-shadow:0 2px 12px rgba(0,0,0,.9);pointer-events:none} +.gcard.dling{border-color:#4a7bd6} .gcov .ph{font:700 22px/1.1 "Segoe UI",system-ui,sans-serif;color:#9fb3d1;letter-spacing:.04em;text-align:center;padding:10px} .gcov .fmtb{position:absolute;left:8px;top:8px;font-size:11px;padding:2px 7px;border-radius:6px;background:rgba(10,12,16,.78);color:#dbe7ff;border:1px solid rgba(255,255,255,.12)} .gbody{padding:10px 11px 11px;display:flex;flex-direction:column;gap:5px;flex:1} @@ -669,6 +681,12 @@ body.notwin .winonly{display:none!important}
+
+

🧹 Limpiar restos

+
+

Envíos y bajadas que se quedaron a medias, carpetas temporales y copias que ya no hacen falta, en la consola y en el PC. Te enseño lo que ocupa cada cosa y no se borra nada sin que lo confirmes.

+
+

Hacer sitio

Di cuánto necesitas liberar y te propongo qué borrar: primero restos y envíos a medias, luego copias repetidas de lo mismo y luego instaladores .pkg. Las aplicaciones solo salen como opción, sin marcar. Nada se borra sin que lo confirmes.

@@ -707,11 +725,13 @@ body.notwin .winonly{display:none!important}

Capturas y vídeos

+

Las capturas de pantalla y los vídeos guardados en la consola. Pulsa una para verla grande; marca las que quieras y «Bajar al PC» las guarda en Descargas › PS5 Directo.

+
@@ -1000,7 +1020,7 @@ body.notwin .winonly{display:none!important}
Limpieza de la PS5 -

Busca en /data/ps5directo restos que ya no usa ninguna transferencia de la cola (partes de trabajos quitados, descompresiones a medias...). Te enseña cuánto ocupa cada cosa antes de borrar.

+

Busca envíos y bajadas a medias y restos que ya no usa ninguna transferencia de la cola, en la consola y en el PC. Te enseña cuánto ocupa cada cosa antes de borrar. También está en Espacio.

@@ -1066,7 +1086,12 @@ I18N_RX.push(['^No hay conexión con la consola\\. Comprueba que la PS5 está en Object.assign(I18N_EXACT,{"Estado de la consola": "Console status", "Volver a mirar": "Check again", "Al abrir PS5 Directo, iniciar solos con Payload Manager los que falten": "When PS5 Directo opens, start any missing ones automatically with Payload Manager", "Mirando la consola…": "Checking the console…", "No encuentro la consola en {0}. ¿Está encendida y con el jailbreak hecho? Revisa la IP en Ajustes.": "I can't find the console at {0}. Is it on and jailbroken? Check the IP in Settings.", "Payload Manager está abierto en la consola: PS5 Directo puede iniciar, instalar y reiniciar los payloads desde aquí.": "Payload Manager is open on the console: PS5 Directo can start, install and restart the payloads from here.", "Payload Manager no está abierto en la consola. Ábrelo (es la aplicación «Payload Manager» del menú de inicio de la PS5) y pulsa «Volver a mirar»: así PS5 Directo puede iniciar los payloads desde aquí. Mientras tanto, cárgalos desde Payload Manager en la consola.": "Payload Manager isn't open on the console. Open it (it's the \"Payload Manager\" app on the PS5 home screen) and click \"Check again\": then PS5 Directo can start the payloads from here. Meanwhile, load them from Payload Manager on the console.", "Funciona": "Working", "No está iniciado": "Not started", "En marcha, pero no responde": "Running, but not responding", "Reiniciar": "Restart", "Iniciar": "Start", "Instalar e iniciar": "Install and start", "Está en marcha pero no contesta: se ha colgado o está cargado dos veces. «Reiniciar» lo cierra y lo vuelve a iniciar.": "It's running but not answering: it hung or was loaded twice. \"Restart\" closes it and starts it again.", "No está instalado en Payload Manager: «Instalar e iniciar» baja la última versión de GitHub, la instala allí y la inicia.": "It isn't installed in Payload Manager: \"Install and start\" downloads the latest version from GitHub, installs it there and starts it.", "Sin el FTP no funciona casi nada: enviar, Biblioteca, Archivos de la consola, Copia de seguridad ni Capturas.": "Without FTP almost nothing works: sending, Library, Console files, Backup or Captures.", "Sin BFpilot no se puede descomprimir en la consola, ver lo que ocupa cada carpeta, la pestaña Espacio ni copiar o mover rápido dentro de la consola. Sí se puede enviar descomprimiendo en el PC.": "Without BFpilot you can't extract on the console, see each folder's size, use the Space tab or copy/move quickly inside the console. You can still send by extracting on the PC.", "ftpsrv y BFpilot se cargan solos cada vez que se abre Payload Manager.": "ftpsrv and BFpilot load automatically every time Payload Manager opens.", "¿Quieres que ftpsrv y BFpilot se carguen solos cada vez que enciendas la consola (al abrirse Payload Manager)? Se añaden a su lista de carga automática, sin quitar lo que ya tengas.": "Do you want ftpsrv and BFpilot to load automatically every time you turn on the console (when Payload Manager opens)? They're added to its autoload list, without removing what you already have.", "Cargarlos solos al encender": "Load them automatically at startup", "Iniciando… (unos segundos)": "Starting… (a few seconds)", "Bajando de GitHub, instalando en Payload Manager e iniciando…": "Downloading from GitHub, installing in Payload Manager and starting…", "Cerrando el que estaba colgado y volviendo a iniciarlo…": "Closing the hung one and starting it again…", "Guardando en Payload Manager…": "Saving in Payload Manager…", "Listo: se cargarán solos al encender la consola.": "Done: they'll load automatically when the console starts.", "Estado de la consola y payloads…": "Console status and payloads…", "Pulsa para ver el estado de la consola y arreglar lo que falte": "Click to see the console status and fix what's missing", "Abre Payload Manager en la consola (es una aplicación del menú de inicio de la PS5) y vuelve a intentarlo: PS5 Directo lo usa para cargar los payloads.": "Open Payload Manager on the console (it's an app on the PS5 home screen) and try again: PS5 Directo uses it to load the payloads.", "Primero instala ftpsrv y BFpilot en Payload Manager": "First install ftpsrv and BFpilot in Payload Manager", "Enviar a la PS5": "Send to the PS5"}); I18N_RX.push(...[["^([\\s\\S]*?) iniciado, pero todavía no responde\\. Espera unos segundos y pulsa «Volver a mirar»\\.$", "{0} started, but it isn't responding yet. Wait a few seconds and click \"Check again\"."], ["^([\\s\\S]*?) funcionando\\.$", "{0} working."], ["^([\\s\\S]*?) ya está en marcha pero no responde: pulsa «Reiniciar»$", "{0} is already running but not responding: click \"Restart\""], ["^Iniciado desde Payload Manager: ([\\s\\S]*?)$", "Started from Payload Manager: {0}"]]); I18N_RX.push(...[["^No encuentro el proceso de ([\\s\\S]*?) en Payload Manager para cerrarlo\\. Ciérralo desde Payload Manager en la consola \\(Procesos activos\\) o reinicia la consola, y luego pulsa «Iniciar»\\.$", "I can't find {0}'s process in Payload Manager to close it. Close it from Payload Manager on the console (Active processes) or restart the console, then click \"Start\"."]]); -Object.assign(I18N_EXACT,{"🎬 Tus clips están en WEBM: Telegram, WhatsApp y muchas webs no los reproducen directamente. Para que los nuevos se graben en MP4, en la PS5 ve a Ajustes → Capturas y emisiones → Capturas → Formato de los videoclips → Tipo de archivo y elige «Más compatible (MP4)» (con la resolución de grabación en 1920 × 1080; en 4K solo hay WebM).": "🎬 Your clips are WEBM: Telegram, WhatsApp and many websites don't play them directly. To record new ones as MP4, on the PS5 go to Settings → Captures and Broadcasts → Captures → Video Clip Format → File Type and choose \"Most Compatible (MP4)\" (with the recording resolution at 1920 × 1080; 4K only offers WebM).", "🌗 ¿Se ven lavados (sin contraste) al compartirlos? Pasa con las capturas y clips grabados en HDR, que el móvil, Telegram o las webs no saben mostrar. Para que se vean bien en todas partes, en la PS5 ve a Ajustes → Capturas y emisiones → Capturas y desactiva «HDR» en el formato de las capturas de pantalla y de los videoclips.": "🌗 Do they look washed out (no contrast) when you share them? It happens with screenshots and clips recorded in HDR, which phones, Telegram and websites can't display properly. To make them look right everywhere, on the PS5 go to Settings → Captures and Broadcasts → Captures and turn off \"HDR\" in the screenshot and video clip formats.", "Entendido": "Got it"}); +Object.assign(I18N_EXACT,{"🎬 Tus clips están en WEBM: Telegram, WhatsApp y muchas webs no los reproducen directamente. Para que los nuevos se graben en MP4, en la PS5 ve a Ajustes → Capturas y emisiones → Capturas → Formato de los videoclips → Tipo de archivo y elige «Más compatible (MP4)» (con la resolución de grabación en 1920 × 1080; en 4K solo hay WebM). Para los que ya tienes, márcalos y pulsa «Convertir a MP4».": "🎬 Your clips are WEBM: Telegram, WhatsApp and many websites don't play them directly. To record new ones as MP4, on the PS5 go to Settings → Captures and Broadcasts → Captures → Video Clip Format → File Type and choose \"Most Compatible (MP4)\" (with the recording resolution at 1920 × 1080; 4K only offers WebM). For the ones you already have, tick them and click \"Convert to MP4\".", "🌗 ¿Se ven lavados (sin contraste) al compartirlos? Pasa con las capturas y clips grabados en HDR, que el móvil, Telegram o las webs no saben mostrar. Para que se vean bien en todas partes, en la PS5 ve a Ajustes → Capturas y emisiones → Capturas y desactiva «HDR» en el formato de las capturas de pantalla y de los videoclips. Los vídeos que ya tienes en HDR, «Convertir a MP4» los deja con colores normales.": "🌗 Do they look washed out (no contrast) when you share them? It happens with screenshots and clips recorded in HDR, which phones, Telegram and websites can't display properly. To make them look right everywhere, on the PS5 go to Settings → Captures and Broadcasts → Captures and turn off \"HDR\" in the screenshot and video clip formats. For HDR videos you already have, \"Convert to MP4\" gives them normal colours.", "Entendido": "Got it"}); +Object.assign(I18N_EXACT,{"Lo que ocupa en la consola": "Size on the console"}); +Object.assign(I18N_EXACT,{"Busca envíos y bajadas a medias y restos que ya no usa ninguna transferencia de la cola, en la consola y en el PC. Te enseña cuánto ocupa cada cosa antes de borrar. También está en Espacio.": "Finds half-finished sends and downloads and leftovers no transfer in the queue uses anymore, on the console and on the PC. It shows how much each one takes before deleting. It's also in Space.", "Vídeos a MP4, y sin HDR para que no se vean lavados": "Videos to MP4, without HDR so they don't look washed out", "🎬 Convertir a MP4": "🎬 Convert to MP4", "Convertir a MP4": "Convert to MP4", "Convertir a MP4 ({0})": "Convert to MP4 ({0})", "Marca algún vídeo": "Tick a video first", "Elige algún vídeo": "Choose a video", "Para convertir hace falta FFmpeg, un programa gratuito de vídeo. Se descarga una sola vez (33 MB, de GitHub) y se guarda en la carpeta de PS5 Directo. ¿Instalarlo y convertir?": "Converting needs FFmpeg, a free video program. It's downloaded only once (33 MB, from GitHub) and kept in the PS5 Directo folder. Install it and convert?", "Para convertir hace falta FFmpeg. Instálalo con Homebrew (brew.sh): brew install ffmpeg": "Converting needs FFmpeg. Install it with Homebrew (brew.sh): brew install ffmpeg", "Para convertir hace falta FFmpeg. Instálalo (por ejemplo: sudo apt install ffmpeg) y vuelve a probar.": "Converting needs FFmpeg. Install it (for example: sudo apt install ffmpeg) and try again.", "Convirtiendo {0} de {1}": "Converting {0} of {1}", "Bajando de la consola": "Downloading from the console", "Convirtiendo a MP4": "Converting to MP4", "Convirtiendo a MP4 y quitando el HDR (tarda un poco más)": "Converting to MP4 and removing HDR (takes a bit longer)", "Instalando el convertidor (FFmpeg, 33 MB, solo la primera vez)": "Installing the converter (FFmpeg, 33 MB, first time only)", "Conversión cancelada": "Conversion cancelled", "{0} vídeos en MP4 en Descargas › PS5 Directo › Capturas, listos para compartir.": "{0} videos as MP4 in Downloads › PS5 Directo › Capturas, ready to share.", "No se pudo convertir": "Couldn't convert", "Ya se está convirtiendo": "Already converting", "🧹 Limpiar restos": "🧹 Clean up leftovers", "Buscar restos": "Find leftovers", "Envíos y bajadas que se quedaron a medias, carpetas temporales y copias que ya no hacen falta, en la consola y en el PC. Te enseño lo que ocupa cada cosa y no se borra nada sin que lo confirmes.": "Sends and downloads that were left half-finished, temporary folders and copies that aren't needed anymore, on the console and on the PC. I show you how much each one takes and nothing is deleted without your confirmation.", "Buscando restos en la consola y en el PC… (puede tardar unos segundos)": "Looking for leftovers on the console and on the PC… (may take a few seconds)", "No hay restos: todo limpio.": "No leftovers: all clean.", "En la consola": "On the console", "En el PC": "On the PC", "No se puede borrar ahora: {0}": "Can't delete it now: {0}", "Borrar marcados ({0})": "Delete ticked ({0})", "¿Borrar {0} elementos ({1})? No se puede deshacer.": "Delete {0} items ({1})? This can't be undone.", "Borrando…": "Deleting…", "Liberados {0}": "{0} freed", "algo se está bajando ahora": "something is downloading right now", "Copias guardadas para ver las capturas (se vuelven a bajar al abrirlas)": "Copies kept to view captures (downloaded again when you open them)", "BFpilot no responde: en la consola solo miro los archivos a medias, no la carpeta de trabajo.": "BFpilot isn't responding: on the console I only check half-sent files, not the work folder.", "No es una aplicación completa. Márcala solo si sabes que es un envío que se cortó.": "It isn't a complete app. Tick it only if you know it's a send that got cut off."}); +I18N_RX.push(...[["^Bajada a medias \\(Bajado de la Biblioteca\\): ([\\s\\S]*?)$", "Half-finished download (from the Library): {0}"], ["^Bajada a medias \\(Descargas\\): ([\\s\\S]*?)$", "Half-finished download (Downloads): {0}"], ["^Bajada a medias \\(Capturas\\): ([\\s\\S]*?)$", "Half-finished file (Captures): {0}"], ["^Temporal de transferencias por el PC: ([\\s\\S]*?)$", "Temporary files of transfers through the PC: {0}"], ["^Descompresión a medias en el PC: ([\\s\\S]*?)$", "Half-finished extraction on the PC: {0}"], ["^Envío a medias: ([\\s\\S]*?)$", "Half-sent: {0}"], ["^Carpeta sin sce_sys \\(¿envío cortado\\?\\): ([\\s\\S]*?)$", "Folder without sce_sys (send cut off?): {0}"], ["^puede que lo use «([\\s\\S]*?)» de la cola$", "\"{0}\" in the queue may be using it"], ["^lo usa «([\\s\\S]*?)» de la cola$", "used by \"{0}\" in the queue"], ["^No pude revisar la consola: ([\\s\\S]*?)$", "Couldn't check the console: {0}"], ["^FFmpeg falló: ([\\s\\S]*?)$", "FFmpeg failed: {0}"], ["^No pude instalar FFmpeg \\(([\\s\\S]*?)\\)\\. Puedes bajarlo de gyan\\.dev\\/ffmpeg y copiar ffmpeg\\.exe en ([\\s\\S]*?)$", "Couldn't install FFmpeg ({0}). You can download it from gyan.dev/ffmpeg and copy ffmpeg.exe into {1}"]]); +Object.assign(I18N_EXACT,{"macOS no deja a PS5 Directo usar la red local. Ve a Ajustes del Sistema → Privacidad y seguridad → Red local, activa «PS5 Directo» (o «python3») y vuelve a abrir el programa.": "macOS isn't letting PS5 Directo use the local network. Go to System Settings → Privacy & Security → Local Network, turn on \"PS5 Directo\" (or \"python3\") and open the program again."}); +I18N_RX.push(...[["^macOS\\ no\\ deja\\ a\\ PS5\\ Directo\\ usar\\ la\\ red\\ local\\.\\ Ve\\ a\\ Ajustes\\ del\\ Sistema\\ →\\ Privacidad\\ y\\ seguridad\\ →\\ Red\\ local,\\ activa\\ «PS5\\ Directo»\\ \\(o\\ «python3»\\)\\ y\\ vuelve\\ a\\ abrir\\ el\\ programa\\. \\(([\\s\\S]*?)\\)$", "macOS isn't letting PS5 Directo use the local network. Go to System Settings → Privacy & Security → Local Network, turn on \"PS5 Directo\" (or \"python3\") and open the program again. ({0})"]]); // --- idioma: español (original) o inglés. Traduce el texto de la página y los mensajes del programa. const LANG=(()=>{try{const v=localStorage.getItem('ps5d_lang');if(v==='es'||v==='en')return v}catch(e){} return (navigator.language||'es').toLowerCase().startsWith('es')?'es':'en'})(); @@ -1521,24 +1546,8 @@ $('btnPerms').onclick=async()=>{ `${esc(x.path)}: ${x.changed?`arreglados ${x.changed} de ${x.seen}${x.eboot.length?` (eboot.bin tenía ${esc(x.eboot[0])})`:''}`:'ya estaba bien'}`).join('
'); }catch(e){o.textContent=e.message} }; -// --- limpieza -let cleanItems=[]; -$('btnScan').onclick=async()=>{ - const o=$('cleanOut'); o.textContent='Buscando… (puede tardar unos segundos)'; - try{const r=await api('/api/cleanup/scan',{}); cleanItems=r.items; - if(!cleanItems.length){o.textContent='No hay nada que limpiar.';return} - o.innerHTML=cleanItems.map((it,i)=>``).join('') - +``; - $('btnClean').onclick=async()=>{ - const sel=[...o.querySelectorAll('input[data-i]:checked')].map(x=>cleanItems[+x.dataset.i]); - if(!sel.length) return; - const tot=sel.reduce((a,b)=>a+(b.size||0),0); - if(!confirm(`¿Borrar ${sel.length} elemento(s) de la PS5 (${human(tot)})? No se puede deshacer.`)) return; - o.textContent='Borrando…'; - try{const d=await api('/api/cleanup/delete',{paths:sel.map(x=>x.path)}); o.textContent=`Borrado: ${d.deleted.length} elemento(s), ${human(tot)} liberados.`; checkConn()}catch(e){o.textContent=e.message} - }; - }catch(e){o.textContent=e.message} -}; +// --- limpieza: ahora en Espacio +$('btnScan').onclick=()=>{ try{$('dlg').close()}catch(e){} showTab('Space'); rsdScan(); setTimeout(()=>$('rsdCard').scrollIntoView({behavior:'smooth',block:'start'}),50) }; // --- destinos segun unidades montadas async function loadDests(){try{const r=await api('/api/places',{});$('destList').innerHTML=r.dests.map(d=>`