Compare commits

..
1 Commits
Author SHA1 Message Date
JesterDev 628c3f63e8 1.21.1: payloads automáticos con Payload Manager 2026-10-28 13:06:11 +01:00
7 changed files with 592 additions and 60 deletions

No files matched your search

+30
View File
@@ -323,6 +323,36 @@ Desinstalar. Tus ajustes se conservan por si vuelves a instalarlo.
del cortafuegos de Windows.
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
(por defecto Descargas > PS5 Directo > Biblioteca, se puede cambiar en
«En tu PC») y sale solo en «En tu PC», listo para volver a enviarlo.
- Al pedir LZ4 de una imagen (.exfat...) cuyo título no usa AMPR, el
aviso decía «como carpeta normal» aunque se envía la imagen tal cual.
Ahora lo dice bien y el paso de la cola pone «Sin convertir».
- Payloads automáticos con Payload Manager: si lo tienes abierto en la
consola, al abrir PS5 Directo se inician solos ftpsrv y BFpilot si
faltan (se puede desactivar). La ventana «Estado de la consola» (pulsa
los puntos FTP o BFpilot de arriba) dice qué falta, por qué y qué no
podrás usar, y lo arregla con un clic: Iniciar, Instalar e iniciar (si
no los tienes en Payload Manager) o Reiniciar (si uno se ha colgado o
está cargado dos veces). También puede añadirlos a la carga automática
de Payload Manager para que se carguen solos al encender la consola.
Nunca carga dos veces el mismo.
- Mac: si la app no llegaba a abrirse, no decía nada. Ahora se quita sola
la marca de "descargado de internet" que puede bloquear el Python que
trae dentro, comprueba que arranca y, si algo falla, muestra un aviso
con el error y qué hacer.
- El programa se abre siempre en «Transferencias» (antes volvía a la
última pestaña que usaste y podía confundir).
- Sin conexión con la consola, las pestañas dicen claramente qué pasa y
qué revisar, en vez de un error de Windows como «WinError 10054».
- Capturas: consejos para que tus clips se graben en MP4 (se ven
directamente en Telegram, WhatsApp y las webs) y para que lo grabado en
HDR no se vea lavado al compartirlo (desactivar HDR en las capturas de
la PS5). Se pueden cerrar con «Entendido».
NOVEDADES 1.21.0
- Versión para Mac EN PRUEBAS (PS5Directo-1.21.0-macOS.dmg), para Apple
Silicon e Intel, con Python y 7-Zip incluidos. Aún no se ha probado en
+1
View File
@@ -27,6 +27,7 @@ An all-in-one Windows tool for jailbroken PS5 consoles: it **downloads, converts
- ✅ **Launch check:** if an app closes on launch, it checks the folder (executable, permissions, firmware, backports, LZ4 packing) and shows recent logs and captures
- ✅ **Problem report** in one click (.zip, no passwords) to share when asking for help
- ✅ **Warnings:** your PS5 firmware always visible, speed-drop alerts, "Why is it slow?" network check, and it loads ftpsrv/BFpilot from the app
- ✅ **Payloads on autopilot:** with Payload Manager open on the console, it starts ftpsrv and BFpilot when they're missing, restarts them if they hang, installs them if needed and can add them to Payload Manager's autoload
- ✅ **Self-updating**, with a welcome window and a user guide, in English and Spanish
## Requirements
+31
View File
@@ -338,6 +338,37 @@ settings are kept in case you install it again.
WHAT'S NEW
----------
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
can change it in "On your PC") and shows up in "On your PC" by
itself, ready to send again.
- Automatic payloads with Payload Manager: if it's open on the
console, ftpsrv and BFpilot start by themselves when PS5 Directo
opens if they're missing (can be turned off). The "Console status"
window (click the FTP or BFpilot dots at the top) says what's
missing, why and what you can't use, and fixes it with one click:
Start, Install and start (if Payload Manager doesn't have them) or
Restart (if one hung or was loaded twice). It can also add them to
Payload Manager's autoload so they load when the console starts.
It never loads the same one twice.
- Asking for LZ4 from an image (.exfat...) whose title doesn't use
AMPR said "as a plain folder" although the image is sent as is.
Now it says so, and the queue step shows "No conversion".
- Mac: if the app didn't open, it said nothing. It now removes the
"downloaded from the internet" flag that can block the Python it
includes, checks that it starts and, if something fails, shows a
message with the error and what to do.
- The program always opens on "Transfers" (it used to go back to
the last tab you used, which could be confusing).
- With no connection to the console, the tabs say clearly what's
going on and what to check, instead of a Windows error such as
"WinError 10054".
- Captures: tips so your clips are recorded as MP4 (they play right
inside Telegram, WhatsApp and websites) and so HDR recordings don't
look washed out when shared (turn off HDR in the PS5's capture
settings). They can be dismissed with "Got it".
1.21.0 - Mac version IN TESTING (PS5Directo-1.21.0-macOS.dmg), for Apple
Silicon and Intel, with Python and 7-Zip included. Not tested on
a real Mac yet: any report on GitHub is welcome.
+242 -19
View File
@@ -24,7 +24,7 @@ sys.path.insert(0, HERE)
import engine as E # noqa: E402
import features as F # noqa: E402
VERSION = "1.21.0"
VERSION = "1.21.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"
@@ -62,6 +62,7 @@ DEFAULTS = {
"icon_ver": "", # version cuyo icono ya se dejo en la cache de la ventana
# 1.20
"elf_port": 9021, # cargador de payloads de la consola (etaHEN / elfldr)
"auto_payloads": True, # al abrir, iniciar solos ftpsrv/BFpilot con Payload Manager si faltan
"console_fw": "", # firmware de tu PS5 (p. ej. 13.60) para avisar de lo que no arrancara
"dup_check": True, # preguntar antes de enviar algo que ya esta en la consola
"verify_sums": True, # comprobar .sfv/.md5/.sha1 si vienen con las partes
@@ -70,6 +71,7 @@ DEFAULTS = {
"keep_awake": True, # que el PC no se suspenda mientras hay envios
"welcome_seen": False, # la ventana de bienvenida ya se ha visto
"pc_collection": [], # carpetas del PC o del NAS con contenido para la consola (pestana Biblioteca > Tu PC)
"pc_library_dir": "", # donde llega lo que bajas de la consola desde la Biblioteca (vacio = Descargas/PS5 Directo/Biblioteca)
"backup_dir": "", # copias de seguridad de la consola (vacio = Documentos\\PS5 Directo\\Copias)
"backup_auto": False, # una copia al dia, sola, si la consola esta encendida
"backup_keep": 10, # cuantas copias guardar
@@ -1006,7 +1008,7 @@ def _fm_start(op, items_total, fn, where=None, names=None):
except Exception as e:
traceback.print_exc()
with LOCK:
FMOP["error"] = str(e)
FMOP["error"] = conn_error_text(e)
notify("err", "%s: %s" % (OP_FAIL.get(op, op), what) if what else OP_FAIL.get(op, op),
str(e), link)
finally:
@@ -1235,10 +1237,17 @@ def fm_extract(src, folder, password="", exists="keepboth"):
_fm_start("extract", 1, work, where=parent, names=[first])
def fm_download(paths):
def pc_library_dir():
"""Carpeta del PC donde llega lo que bajas de la consola desde la Biblioteca. Siempre sale en
Biblioteca > En tu PC."""
d = (SETTINGS.get("pc_library_dir") or "").strip().strip('"')
return d or os.path.join(downloads_dir(), "PS5 Directo", "Biblioteca")
def fm_download(paths, dest=None):
cfg = cfg_snapshot()
paths = [fm_clean(x) for x in paths]
root_local = os.path.join(downloads_dir(), "PS5 Directo")
root_local = dest or os.path.join(downloads_dir(), "PS5 Directo")
def walk(rpath):
"""[(ruta remota, ruta relativa, tamano)] de un archivo o carpeta."""
@@ -1290,6 +1299,8 @@ def fm_download(paths):
with LOCK:
FMOP.update(items_done=len(files), bytes_done=done,
message="Guardado en %s" % root_local)
if dest: # bajado a la carpeta de la Biblioteca: que salga ya en «En tu PC»
threading.Thread(target=collection_scan, daemon=True).start()
_fm_start("download", len(paths), work, names=[posixpath.basename(x) for x in paths])
@@ -1465,7 +1476,7 @@ def space_start(root):
except Exception as e:
traceback.print_exc()
with LOCK:
SPACE["error"] = str(e)
SPACE["error"] = conn_error_text(e)
finally:
with LOCK:
SPACE["busy"] = False
@@ -1559,7 +1570,31 @@ def fm_api(p, b):
return {"path": fp, "name": os.path.basename(fp)}
if p == "/api/col/state":
with LOCK:
return {"col": dict(COL), "roots": list(SETTINGS.get("pc_collection") or [])}
return {"col": dict(COL), "roots": list(SETTINGS.get("pc_collection") or []),
"libdir": pc_library_dir(), "libdir_custom": bool(SETTINGS.get("pc_library_dir"))}
if p == "/api/col/libdir":
d = str(b.get("path") or "").strip().strip('"')
if d and not os.path.isabs(d):
raise E.PipeError("Pon una ruta completa, por ejemplo D:\\PS5")
with LOCK:
SETTINGS["pc_library_dir"] = d
save_settings()
with LOCK:
COL["items"] = None
return {"libdir": pc_library_dir()}
if p == "/api/col/openlibdir":
d = pc_library_dir()
os.makedirs(d, exist_ok=True)
_open_quiet(d)
return {"dir": d}
if p == "/api/lib/download":
paths = [str(x) for x in (b.get("paths") or []) if str(x).startswith(("/data/", "/mnt/"))]
if not paths:
raise E.PipeError("No hay nada que bajar")
d = pc_library_dir()
os.makedirs(d, exist_ok=True)
fm_download(paths, dest=d)
return {"dir": d}
if p == "/api/col/scan":
if not COL["busy"]:
threading.Thread(target=collection_scan, daemon=True).start()
@@ -1892,6 +1927,21 @@ MEDIA_TYPES = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
# Diagnostico e informe de fallo
# ---------------------------------------------------------------------------
_CONN_RX = re.compile(r"(?i)10054|10053|10060|10061|10065|connection (reset|refused|aborted)|timed out|"
r"no route|unreachable|broken pipe|forzado la interrupci|denegado expresamente|"
r"no respondi|errno (32|54|60|61|64|65|104|110|111|113)")
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 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)
return t
DIAG_RULES = [
(r"(?i)10061|refused|rechaz|No route|timed out|unreachable|No puedo conectar con el FTP|conexi[oó]n con la PS5",
"No se pudo hablar con la consola por FTP.",
@@ -2249,12 +2299,16 @@ def _local_dir_size(path, limit_s=8.0):
def collection_scan():
"""Lee las carpetas de la coleccion del PC: aplicaciones en carpeta (sce_sys), imagenes, .pkg y comprimidos."""
roots = [r for r in (SETTINGS.get("pc_collection") or []) if r]
lib = pc_library_dir()
roots = [lib] + [r for r in (SETTINGS.get("pc_collection") or [])
if r and os.path.normcase(os.path.abspath(r)) != os.path.normcase(os.path.abspath(lib))]
with LOCK:
COL.update(busy=True, error="", current="")
items = []
try:
for root in roots:
if root == lib and not os.path.isdir(root):
continue # la carpeta de la Biblioteca se crea al bajar lo primero
if not os.path.isdir(root):
items.append({"path": root, "kind": "missing", "name": root})
continue
@@ -2307,7 +2361,7 @@ def collection_scan():
except Exception as e:
traceback.print_exc()
with LOCK:
COL["error"] = str(e)
COL["error"] = conn_error_text(e)
finally:
with LOCK:
COL.update(busy=False, current="")
@@ -2944,10 +2998,13 @@ def forge_job(job, srcs, cfg, cancel, src_path, created, pc_folder, up):
if fmt in ("lz4", "lz4ffpkg") and not lz4.get("imports_ampr"):
jlog(job, "Este título no usa AMPR, así que LZ4 no sirve con él")
if fmt == "lz4":
img = len(srcs) == 1 and IMAGE_RE.search(srcs[0].name or "")
msg = ("PS5 Dump Forge dice que este título no usa AMPR, así que no se puede pasar a LZ4: "
"se envía sin convertir, como carpeta normal.")
+ ("se envía sin convertir, tal cual (%s)." % srcs[0].name if img else
"se envía sin convertir, como carpeta normal."))
with LOCK:
job["warning"] = msg
setj(job, fmt_real="none")
jlog(job, msg)
return None
fmt = "ffpkg"
@@ -4319,7 +4376,8 @@ def gu_status():
try:
port = E.Ftp(cfg).call(look)
except Exception as e:
out["problem"] = "No pude mirarlo: %s" % e
if conn_error_text(e) == str(e): # sin conexion ya lo dice la Biblioteca: aqui no se repite
out["problem"] = "No pude mirarlo: %s" % e
return out
out["url"] = "http://%s:%d/" % (cfg["ps5_ip"], port)
try:
@@ -4652,7 +4710,7 @@ def library_start(refresh=False):
except Exception as e:
traceback.print_exc()
with LOCK:
LIB["error"] = str(e)
LIB["error"] = conn_error_text(e)
finally:
with LOCK:
LIB["busy"] = False
@@ -4883,7 +4941,7 @@ def backup_run(sections=None, label="copia"):
except Exception as e:
traceback.print_exc()
with LOCK:
BK["error"] = str(e)
BK["error"] = conn_error_text(e)
notify("err", "No se pudo hacer la copia de seguridad", str(e))
finally:
try:
@@ -4982,7 +5040,7 @@ def backup_restore(path, ids):
BK["error"] = "Cancelado"
except Exception as e:
with LOCK:
BK["error"] = str(e)
BK["error"] = conn_error_text(e)
notify("err", "No se pudieron restaurar las partidas", str(e))
finally:
with LOCK:
@@ -5080,8 +5138,167 @@ def wait_service(pid, cfg, secs=20):
return False
LOST = {"ftpsrv": "Sin el FTP no funciona casi nada: enviar, Biblioteca, Archivos de la consola, Copia de "
"seguridad ni Capturas.",
"bfpilot": "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."}
CONSOLE = {"auto": None} # lo que hizo el arranque automatico (para ensenarlo en la ventana)
def _port_open(host, port, timeout=1.5):
host = E.clean_host(host or "")[0]
if not host:
return False
try:
socket.create_connection((host, int(port)), timeout=timeout).close()
return True
except OSError:
return False
def _svc_ok(pid, cfg):
if pid == "ftpsrv":
try:
E.Ftp(cfg).connect().quit()
return True, ""
except Exception as e:
return False, str(e)
v = E.BFpilot(cfg).alive()
return bool(v), "" if v else "no responde"
def console_status():
"""Que hay en la consola: FTP, BFpilot, Payload Manager (y si los tiene instalados y en marcha)
y el cargador de ELF por red (9021)."""
cfg = cfg_snapshot()
pm = F.PayloadManager(cfg["ps5_ip"])
ver = pm.version()
items, procs, autoload = [], [], []
pm_err = ""
if ver:
try:
items = pm.payloads()
procs = pm.processes()
autoload = [x.strip() for x in re.split(r"[,\n]", pm.config().get("AUTOLOAD_LIST") or "") if x.strip()]
except Exception as e:
pm_err = str(e)
out = {"ip": cfg["ps5_ip"], "pm": {"present": bool(ver), "version": ver, "error": pm_err},
"loader": _port_open(cfg["ps5_ip"], cfg.get("elf_port") or 9021), "items": [], "auto": CONSOLE["auto"]}
for p in F.PAYLOADS:
ok, err = _svc_ok(p["id"], cfg)
slot = pm.find(p["id"], items) if ver else None
run = pm.running(p["id"], procs) if ver else []
# colgado: Payload Manager lo tiene en marcha, o su puerto acepta conexiones pero no contesta bien
# (por ejemplo un ftpsrv cargado dos veces da "WinError 10054")
port = 2121 if p["id"] == "ftpsrv" else 5905
state = "ok" if ok else ("stuck" if (run or _port_open(cfg["ps5_ip"], port, 1.5)) else "off")
out["items"].append({"id": p["id"], "name": p["name"], "ok": ok, "error": conn_error_text(Exception(err)) if err else "",
"state": state, "installed": bool(slot), "slot": (slot or {}).get("slot"),
"file": (slot or {}).get("filename"), "pids": [x.get("pid") for x in run],
"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))
return out
def console_fix(pid, action):
"""Arregla un payload con Payload Manager: 'start' (iniciarlo), 'install' (instalarlo e iniciarlo),
'restart' (cerrar el que esta colgado o repetido y volver a iniciarlo) o 'autoload'."""
cfg = cfg_snapshot()
pm = F.PayloadManager(cfg["ps5_ip"])
if not pm.version():
if action in ("start", "install") and _port_open(cfg["ps5_ip"], cfg.get("elf_port") or 9021):
return payload_send(pid=pid)
raise E.PipeError("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.")
if action == "autoload":
st = console_status()
slots = [i["slot"] for i in st["items"] if i["slot"]]
if not slots:
raise E.PipeError("Primero instala ftpsrv y BFpilot en Payload Manager")
pm.add_autoload(slots)
return {"done": "autoload"}
p = next((x for x in F.PAYLOADS if x["id"] == pid), None)
if not p:
raise E.PipeError("Payload desconocido")
slot = pm.find(pid)
if action == "install" or not slot:
path, tag = F.fetch_payload(pid, DATA_DIR, log=print)
with open(path, "rb") as fh:
pm.upload("%s_%s.elf" % (p["name"], re.sub(r"[^\w.-]", "_", tag or "")), fh.read(64 * 1024 * 1024))
slot = pm.find(pid)
if not slot:
raise E.PipeError("Payload Manager no muestra %s después de instalarlo" % p["name"])
port = 2121 if pid == "ftpsrv" else 5905
if action == "restart":
if not pm.running(pid):
raise E.PipeError("No encuentro el proceso de %s en Payload Manager para cerrarlo. Ciérralo desde "
"Payload Manager en la consola (Procesos activos) o reinicia la consola, y luego "
"pulsa «Iniciar»." % p["name"])
for x in pm.running(pid):
try:
pm.kill(x["pid"])
except Exception as e:
print("No pude cerrar %s (%s): %s" % (x.get("name"), x.get("pid"), e))
time.sleep(2)
elif (pm.running(pid) or _port_open(cfg["ps5_ip"], port, 1.5)) and not _svc_ok(pid, cfg)[0]:
raise E.PipeError("%s ya está en marcha pero no responde: pulsa «Reiniciar»" % p["name"])
pm.launch(slot["slot"])
up = wait_service(pid, cfg)
try:
test_connection(cfg)
except Exception:
pass
return {"name": p["name"], "up": up}
def console_auto():
"""Al abrir PS5 Directo: si falta ftpsrv o BFpilot y Payload Manager esta abierto en la consola,
los inicia solo (solo los que no esten ya en marcha: nunca dos veces lo mismo)."""
with LOCK:
on = SETTINGS.get("auto_payloads", True)
if not on:
return
try:
st = console_status()
except Exception as e:
print("Comprobación de la consola:", e)
return
done = []
if st["pm"]["present"]:
for it in st["items"]:
if it["ok"] or it["state"] != "off" or not it["installed"]:
continue
try:
r = console_fix(it["id"], "start")
done.append({"id": it["id"], "name": it["name"], "up": r.get("up")})
except Exception as e:
done.append({"id": it["id"], "name": it["name"], "error": str(e)})
CONSOLE["auto"] = {"t": time.time(), "done": done}
if done:
ok = [d["name"] for d in done if d.get("up")]
if ok:
notify("info", "Iniciado desde Payload Manager: %s" % ", ".join(ok), "")
def payload_send(pid=None, path=None):
cfg = cfg_snapshot()
if pid and F.PayloadManager(cfg["ps5_ip"]).version():
# con Payload Manager abierto en la consola, lo carga el (el cargador 9021 suele ser solo local)
r = console_fix(pid, "start")
return {"name": r.get("name") or pid, "tag": "", "up": r.get("up")}
if path and F.PayloadManager(cfg["ps5_ip"]).version():
pm = F.PayloadManager(cfg["ps5_ip"])
if not os.path.isfile(path):
raise E.PipeError("No existe ese archivo")
name = os.path.basename(path)
with open(path, "rb") as fh:
pm.upload(name, fh.read(64 * 1024 * 1024))
hit = next((x for x in pm.payloads() if x.get("filename") == name), None)
if hit:
pm.launch(hit["slot"])
return {"name": name, "tag": "", "up": None}
if pid:
path, tag = F.fetch_payload(pid, DATA_DIR, log=print)
name = next(p["name"] for p in F.PAYLOADS if p["id"] == pid)
@@ -5106,9 +5323,10 @@ def payload_send(pid=None, path=None):
LAN = {"srv": None, "port": None, "error": ""}
LAN_HITS = [] # lo ultimo que ha pedido el movil: para saber si llega y si se corta
LAN_DENY = {"/api/bk/restore", "/api/bk/open", "/api/report/open", "/api/col/set", "/api/lan/firewall", "/api/lan/hits", "/api/update/zip", "/api/update/github", "/api/quit", "/api/pick", "/api/open_ext_folder",
LAN_DENY = {"/api/bk/restore", "/api/bk/open", "/api/report/open", "/api/col/set", "/api/col/libdir",
"/api/col/openlibdir", "/api/lan/firewall", "/api/lan/hits", "/api/update/zip", "/api/update/github", "/api/quit", "/api/pick", "/api/open_ext_folder",
"/api/fm/open_local", "/api/lan", "/api/forge/download", "/api/payload/file", "/api/fixperms"}
LAN_LOCKED = {"sevenzip_path", "forge_path", "pc_temp_dir", "update_repo", "lan_access", "lan_port", "lan_token"}
LAN_LOCKED = {"sevenzip_path", "forge_path", "pc_temp_dir", "pc_library_dir", "update_repo", "lan_access", "lan_port", "lan_token"}
def token_ok(tok):
@@ -6072,22 +6290,22 @@ class Handler(BaseHTTPRequestHandler):
return self._send(200, {"ok": False, "error": str(e)})
except Exception as e:
traceback.print_exc()
return self._send(200, {"ok": False, "error": str(e)})
return self._send(200, {"ok": False, "error": conn_error_text(e)})
if p == "/api/fixperms":
try:
return self._send(200, {"ok": True, "results": fix_all_perms()})
except Exception as e:
return self._send(500, {"ok": False, "error": str(e)})
return self._send(500, {"ok": False, "error": conn_error_text(e)})
if p == "/api/cleanup/scan":
try:
return self._send(200, {"ok": True, "items": cleanup_scan()})
except Exception as e:
return self._send(500, {"ok": False, "error": str(e)})
return self._send(500, {"ok": False, "error": conn_error_text(e)})
if p == "/api/cleanup/delete":
try:
return self._send(200, {"ok": True, "deleted": cleanup_delete(b.get("paths") or [])})
except Exception as e:
return self._send(500, {"ok": False, "error": str(e)})
return self._send(500, {"ok": False, "error": conn_error_text(e)})
if p == "/api/speedtest":
if SPEEDTEST["running"]:
return self._send(409, {"ok": False, "error": "Ya hay un test en marcha"})
@@ -6230,6 +6448,10 @@ def extra_api(h, p, b):
return dict(ok=True, **payload_send(pid=str(b.get("id") or "")))
if p == "/api/payload/file":
return dict(ok=True, **payload_send(path=str(b.get("path") or "")))
if p == "/api/console/status":
return dict(ok=True, **console_status())
if p == "/api/console/fix":
return dict(ok=True, **console_fix(str(b.get("id") or ""), str(b.get("action") or "start")))
if p == "/api/library/scan":
library_start(bool(b.get("refresh")))
return {"ok": True}
@@ -6891,6 +7113,7 @@ def main():
except Exception:
pass
threading.Thread(target=ensure_shortcuts, daemon=True).start()
threading.Thread(target=console_auto, daemon=True).start() # payloads que falten, con Payload Manager
refresh_window_icon()
if SETTINGS.get("lan_access"):
try:
+86
View File
@@ -772,6 +772,92 @@ def fetch_payload(pid, data_dir, log=None):
return dest, tag
PLDMGR_PORT = 8084
PAYLOAD_RX = {"ftpsrv": re.compile(r"ftpsrv", re.I), "bfpilot": re.compile(r"bfpilot", re.I)}
class PayloadManager:
"""Panel web de Payload Manager (itsPLK/ps5-payload-manager) en la consola, puerto 8084.
Es lo que usa casi todo el mundo para cargar payloads: desde el PC se le puede pedir que
instale, lance o cierre payloads y que los cargue solos al encender (su lista de autoload).
Sus acciones son POST y piden la cabecera X-PLDMGR-Request: 1 (su proteccion anti-CSRF)."""
def __init__(self, host, port=PLDMGR_PORT):
self.host = E.clean_host(host or "")[0]
self.port = int(port or PLDMGR_PORT)
def _req(self, method, path, body=None, ctype=None, timeout=8):
if not self.host:
raise E.PipeError("Falta la IP de la PS5: ponla en Ajustes")
url = "http://%s:%d%s" % (self.host, self.port, path)
headers = {"User-Agent": "PS5Directo"}
if method == "POST":
headers["X-PLDMGR-Request"] = "1"
if ctype:
headers["Content-Type"] = ctype
req = urllib.request.Request(url, data=body if method == "POST" else None, method=method,
headers=headers)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.read(8 * MiB)
except urllib.error.HTTPError as e:
msg = e.read(2000).decode("utf-8", "replace").strip()
raise E.PipeError("Payload Manager respondió %d: %s" % (e.code, msg[:200]))
def _json(self, path, timeout=8):
return json.loads(self._req("GET", path, timeout=timeout).decode("utf-8", "replace") or "{}")
def version(self, timeout=3):
"""Version de Payload Manager, o None si no esta abierto en la consola."""
try:
v = self._req("GET", "/version", timeout=timeout).decode("utf-8", "replace").strip()
return v or "?"
except Exception:
return None
def payloads(self):
return self._json("/list_payloads").get("items") or []
def processes(self):
return self._json("/processes_list").get("processes") or []
def config(self):
return self._json("/get_config")
def find(self, pid, items=None):
"""El payload instalado que corresponde a 'ftpsrv' o 'bfpilot' (el mas nuevo si hay varios)."""
rx = PAYLOAD_RX[pid]
items = self.payloads() if items is None else items
hits = [x for x in items if rx.search(x.get("filename") or "") or rx.search(x.get("name") or "")]
if pid == "ftpsrv": # el ftpsrv "normal" antes que otras versiones
hits.sort(key=lambda x: "drakmor" in (x.get("filename") or "").lower())
return hits[0] if hits else None
def running(self, pid, procs=None):
rx = PAYLOAD_RX[pid]
procs = self.processes() if procs is None else procs
return [p for p in procs if rx.search(p.get("name") or "")]
def launch(self, slot):
self._req("POST", "/loadpayload:" + urllib.parse.quote(slot, safe="/:"), timeout=30)
def kill(self, pid):
self._req("POST", "/process_kill?pid=%d" % int(pid), timeout=15)
def upload(self, filename, data):
q = urllib.parse.urlencode({"filename": filename, "mode": "new"})
self._req("POST", "/manage:upload?" + q, body=data, ctype="application/octet-stream", timeout=60)
def add_autoload(self, slots):
"""Anade esos payloads a su lista de carga automatica (sin quitar lo que ya tuviera) y la activa."""
cfg = self.config()
cur = [x.strip() for x in re.split(r"[,\n]", cfg.get("AUTOLOAD_LIST") or "") if x.strip()]
new = cur + [s for s in slots if s and s not in cur]
body = json.dumps({"AUTOLOAD_LIST": ",".join(new), "AUTOLOAD_ENABLED": True}).encode()
self._req("POST", "/set_config", body=body, ctype="application/json", timeout=15)
return new
def send_elf(host, port, data, timeout=15):
"""Manda un payload al cargador de ELF de la consola (elfldr/etaHEN, puerto 9021)."""
host = E.clean_host(host)[0]
+81 -10
View File
@@ -4,17 +4,64 @@
# - El programa se copia a ~/Library/Application Support/PS5 Directo/app y se ejecuta desde ahi,
# para que «Actualizar» pueda escribir aunque la app este en /Applications. Si la app trae una
# version mas nueva que la copiada, se vuelve a copiar.
# - Si algo impide arrancar, muestra un aviso con el error (en vez de no hacer nada).
RES="$(cd "$(dirname "$0")/../Resources" && pwd)"
BUNDLE="$(cd "$RES/../.." && pwd)"
SUP="$HOME/Library/Application Support/PS5 Directo"
APP="$SUP/app"
LOG="$SUP/lanzador.log"
mkdir -p "$SUP"
PLOG="$HOME/.ps5directo/ps5directo.log"
mkdir -p "$SUP" "$HOME/.ps5directo"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG"; }
# Aviso en pantalla (en Linux, para pruebas, solo lo escribe)
alert() {
log "AVISO: $1"
if command -v osascript >/dev/null 2>&1; then
osascript -e 'on run argv' -e 'display alert "PS5 Directo" message (item 1 of argv) as critical' \
-e 'end run' "$1" >/dev/null 2>&1
else
echo "PS5 Directo: $1" >&2
fi
}
HELP="Si no se arregla, abre Terminal y escribe:
xattr -dr com.apple.quarantine \"$BUNDLE\"
y vuelve a abrir la app. Si sigue igual, avisa en github.com/jstrdev/ps5directo/issues con lo de arriba.
If it doesn't fix itself, open Terminal and run the command above, then open the app again. If it still fails, please report it at github.com/jstrdev/ps5directo/issues with the text above."
log "inicio: app en $BUNDLE, procesador $(uname -m), macOS $(sw_vers -productVersion 2>/dev/null)"
case "$BUNDLE" in */AppTranslocation/*) log "la app se ejecuta desde una copia temporal de macOS (no está en Aplicaciones)";; esac
# macOS marca como "descargado de internet" todo lo que trae la app y puede bloquear en silencio el
# Python de dentro. La propia app se quita esa marca (si macOS lo permite).
if command -v xattr >/dev/null 2>&1; then
xattr -dr com.apple.quarantine "$BUNDLE" 2>/dev/null || xattr -dr com.apple.quarantine "$RES" 2>/dev/null
fi
case "$(uname -m)" in
arm64) PY="$RES/python-arm64/bin/python3" ;;
*) PY="$RES/python-x86_64/bin/python3" ;;
esac
[ -x "$PY" ] || PY="$RES/python-x86_64/bin/python3" # Rosetta, por si acaso
if [ ! -x "$PY" ]; then
alert "No encuentro el Python incluido en la app. Vuelve a descargarla de github.com/jstrdev/ps5directo/releases
The Python included in the app is missing. Please download it again."
exit 1
fi
# Python funciona?
OUT="$("$PY" -c 'import ssl, sqlite3, json; print("ok")' 2>&1)"; RC=$?
if [ $RC -ne 0 ] || [ "$OUT" != "ok" ]; then
alert "macOS no deja arrancar el Python incluido en la app (código $RC).
macOS is blocking the Python included in the app (code $RC).
$(printf '%s' "$OUT" | tail -n 6)
$HELP"
exit 1
fi
ver() { sed -n 's/^VERSION = "\([^"]*\)".*/\1/p' "$1" 2>/dev/null | head -1; }
newer() { # 0 si $1 es mas nueva que $2
@@ -25,21 +72,45 @@ newer() { # 0 si $1 es mas nueva que $2
BUNDLED="$(ver "$RES/app/app.py")"
COPIED="$(ver "$APP/app.py")"
if [ ! -f "$APP/app.py" ] || newer "$BUNDLED" "$COPIED"; then
echo "$(date) copio la version $BUNDLED (habia: ${COPIED:-ninguna})" >> "$LOG"
log "copio la version $BUNDLED (habia: ${COPIED:-ninguna})"
rm -rf "$APP.nuevo"
cp -R "$RES/app" "$APP.nuevo" && rm -rf "$APP" && mv "$APP.nuevo" "$APP"
if ! { cp -R "$RES/app" "$APP.nuevo" && rm -rf "$APP" && mv "$APP.nuevo" "$APP"; }; then
alert "No pude copiar el programa a $SUP. ¿Hay espacio libre en el Mac?
Couldn't copy the program to $SUP. Is there free space on the Mac?"
exit 1
fi
command -v xattr >/dev/null 2>&1 && xattr -dr com.apple.quarantine "$APP" 2>/dev/null
fi
export PATH="$RES/bin:$PATH" # 7-Zip incluido
export PYTHONUNBUFFERED=1
cd "$APP" || exit 1
if [ ! -x "$PY" ]; then
osascript -e 'display alert "PS5 Directo" message "No encuentro el Python incluido en la app. Vuelve a descargarla de github.com/jstrdev/ps5directo/releases"' >/dev/null 2>&1
exit 1
fi
mkdir -p "$HOME/.ps5directo"
# El programa sigue en segundo plano y el lanzador termina: asi cada clic en el icono vuelve a
# entrar aqui y, si ya estaba abierto, solo trae su ventana (si no, macOS creeria que la app
# sigue abierta y no haria nada).
nohup "$PY" "$APP/app.py" "$@" >> "$HOME/.ps5directo/ps5directo.log" 2>&1 < /dev/null &
# sigue abierta y no haria nada). Antes de terminar, espera a ver que arranca bien.
START=$(wc -c < "$PLOG" 2>/dev/null | tr -d ' '); START=${START:-0}
nohup "$PY" "$APP/app.py" "$@" >> "$PLOG" 2>&1 < /dev/null &
PID=$!
log "arranco el programa (proceso $PID)"
for i in $(seq 1 40); do
sleep 0.5
NEW="$(tail -c +$((START + 1)) "$PLOG" 2>/dev/null)"
case "$NEW" in *"Abre en el navegador"*) log "arrancado"; exit 0;; esac
if ! kill -0 "$PID" 2>/dev/null; then
wait "$PID"; RC=$?
if [ $RC -eq 0 ] || [ $RC -eq 3 ]; then # 3 = ya estaba abierto: solo se ha traido su ventana
log "ya estaba abierto (código $RC)"; exit 0
fi
alert "PS5 Directo se ha cerrado al arrancar (código $RC). Esto es lo último que escribió:
PS5 Directo closed while starting (code $RC). Last lines:
$(printf '%s' "$NEW" | tail -n 12)
$HELP"
exit 1
fi
done
log "sigue arrancando (tarda más de lo normal)"
exit 0
+121 -31
View File
@@ -323,6 +323,8 @@ tr:hover .dots{opacity:1}
.bkkeep{display:flex;align-items:center;gap:8px;font-size:13.5px}.bkkeep input{width:64px}
.bkprog{margin:8px 0 12px}
.mdgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-top:10px}
.md-tip{display:flex;gap:10px;align-items:flex-start;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);margin:8px 0;font-size:13px;line-height:1.45}
.md-tip>div{flex:1}.md-tip button{flex:none}
.mdt{position:relative;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--bg);cursor:pointer;aspect-ratio:16/9;display:grid;place-items:center}
.mdt img{width:100%;height:100%;object-fit:cover;display:block}
.mdt .vd{font-size:34px;color:#fff;position:relative;text-shadow:0 2px 10px rgba(0,0,0,.8)}
@@ -332,6 +334,10 @@ tr:hover .dots{opacity:1}
.mdt input{position:absolute;top:7px;left:7px;width:18px;height:18px}
.mdt.on{outline:2px solid #5b9dff;outline-offset:-2px}
.colroots{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0 10px}
.collib{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);margin:8px 0 4px;font-size:13px}
.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}
.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}
@@ -382,6 +388,10 @@ header h1{white-space:nowrap}
.diag-i .ic.ok{color:#4ade80}.diag-i .ic.warn{color:#ffcf6b}.diag-i .ic.bad{color:#f87171}.diag-i .ic.info{color:#9ec5ff}
.pl-row{display:flex;gap:12px;align-items:center;padding:10px 0;border-bottom:1px solid rgba(42,50,61,.6)}
.pl-row>div{flex:1;min-width:0}
.pl-st{display:inline-block;font-size:12px;font-weight:600;padding:1px 8px;border-radius:999px;margin-left:6px}
.pl-st.ok{background:rgba(46,160,67,.18);color:#7ee2a0}.pl-st.off{background:rgba(220,70,70,.16);color:#ff9a9a}.pl-st.stuck{background:rgba(230,170,40,.18);color:#ffd36b}
.pl-box{padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);margin:4px 0 8px;font-size:13px}
.pl-lost{color:#ffb1b1}
.qr{background:#fff;border-radius:10px;padding:8px;display:inline-block;margin-top:10px}
.qr svg{display:block;width:220px;height:220px}
.qr.qrs svg{width:120px;height:120px}
@@ -451,8 +461,8 @@ body.notwin .winonly{display:none!important}
<div class="sub">Envía archivos a tu consola por la red local, sin pasar por el disco del PC</div>
</div>
<div class="spacer"></div>
<span class="pill click" id="pFtp" title="Pulsa para cargar payloads en la PS5"><span class="dot"></span>FTP</span>
<span class="pill click" id="pBf" title="Pulsa para cargar payloads en la PS5"><span class="dot"></span>BFpilot</span>
<span class="pill click" id="pFtp" title="Pulsa para ver el estado de la consola y arreglar lo que falte"><span class="dot"></span>FTP</span>
<span class="pill click" id="pBf" title="Pulsa para ver el estado de la consola y arreglar lo que falte"><span class="dot"></span>BFpilot</span>
<span class="pill" id="pFree" hidden></span>
<button class="small bell" id="btnBell" title="Notificaciones"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg><span class="badge" id="bellN" hidden></span></button>
<button class="small" id="btnLang" data-noi18n title="Idioma / Language"></button>
@@ -556,6 +566,7 @@ body.notwin .winonly{display:none!important}
<button class="primary small" id="libScan">Actualizar lista</button></div>
<p class="hint">Lo que hay en /data/homebrew (y en los USB): portada, versión, si es carpeta o imagen y qué firmware pide. Con el firmware de tu PS5 en Ajustes te marca los que no arrancarán. Funciona solo con FTP; con BFpilot también ve cuánto ocupa cada carpeta. Si a algo le falta la portada o el nombre, se buscan en lo que la PS5 guarda de cada aplicación de su menú.</p>
<div id="libGU" hidden></div>
<div id="libDl" hidden></div>
<div class="lib-tools">
<input id="libSearch" placeholder="Buscar por nombre o Title ID…">
<select id="libFilter">
@@ -585,7 +596,8 @@ body.notwin .winonly{display:none!important}
<div class="fm-head"><h2>En tu PC</h2><div class="spacer"></div>
<button class="small" id="colAdd">Añadir carpeta…</button>
<button class="primary small" id="colScan">Volver a leer</button></div>
<p class="hint">Elige las carpetas de tu PC o de tu NAS donde guardas contenido para la consola (también valen rutas de red como \\NAS\carpeta). Te marca lo que ya está en la consola y lo que no, y lo envías con un clic (con el formato y el destino elegidos en Transferencias).</p>
<p class="hint">Lo que tienes en tu PC o en tu NAS para la consola. Te marca lo que ya está en la consola y lo que no, y lo envías con un clic (con el formato y el destino elegidos en Transferencias). Lo que bajas de la consola con «Bajar al PC» en la Biblioteca llega a la carpeta de PS5 Directo y sale aquí solo. Puedes añadir otras carpetas tuyas (también valen rutas de red como \\NAS\carpeta).</p>
<div class="collib" id="colLib"></div>
<div class="colroots" id="colRoots"></div>
<div class="lib-sum" id="colSum"></div>
<div class="lib-grid" id="colGrid"></div>
@@ -699,6 +711,7 @@ body.notwin .winonly{display:none!important}
<button class="primary small" id="mdLoad">Actualizar</button></div>
<p class="hint">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.</p>
<div class="hint" id="mdSum"></div>
<div id="mdTips"></div>
<div class="mdgrid" id="mdGrid"></div>
</section>
</main>
@@ -854,11 +867,14 @@ body.notwin .winonly{display:none!important}
</div>
</dialog>
<dialog id="dlgPayloads">
<div class="head"><h2>Cargar payloads en la PS5</h2><div class="spacer"></div><button class="small" id="plClose">Cerrar</button></div>
<div class="head"><h2>Estado de la consola</h2><div class="spacer"></div><button class="small" id="plRefresh">Volver a mirar</button><button class="small" id="plClose">Cerrar</button></div>
<div class="body">
<p class="hint">Manda el payload al cargador de la consola (etaHEN o elfldr, puerto <span id="plPort">9021</span>). Se descarga la última versión de GitHub y se guarda para la próxima vez. El cargador de Y2JB Autoloader solo acepta la propia consola: con él usa Payload Manager.</p>
<div id="plPm"></div>
<div id="plList"></div>
<div id="plAuto"></div>
<label class="check" style="margin-top:8px"><input type="checkbox" id="plAutoStart"> Al abrir PS5 Directo, iniciar solos con Payload Manager los que falten</label>
<div class="actions"><button class="small" id="plFile">Enviar otro .elf del PC…</button><span class="hint" id="plOut"></span></div>
<span id="plPort" hidden></span>
</div>
</dialog>
<dialog id="dlgDiag">
@@ -951,7 +967,7 @@ body.notwin .winonly{display:none!important}
</div>
<div style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px">
<b>Herramientas</b>
<div class="actions" style="margin-top:8px"><button class="small" id="btnSetupOpen">Asistente de primeros pasos</button><button class="small" id="btnPlOpen">Cargar payloads en la PS5…</button><button class="small" id="btnDiag">¿Por qué va lento?</button><button class="small" id="btnLanOpen">Abrir en el móvil…</button></div>
<div class="actions" style="margin-top:8px"><button class="small" id="btnSetupOpen">Asistente de primeros pasos</button><button class="small" id="btnPlOpen">Estado de la consola y payloads…</button><button class="small" id="btnDiag">¿Por qué va lento?</button><button class="small" id="btnLanOpen">Abrir en el móvil…</button></div>
</div>
<div class="actions">
<button class="primary" id="btnSave">Guardar</button>
@@ -997,7 +1013,7 @@ body.notwin .winonly{display:none!important}
<script>
const I18N_EXACT={"Atrás (Alt+←)": "Back (Alt+←)", "Adelante (Alt+→)": "Forward (Alt+→)", "Subir un nivel (Retroceso)": "Up one level (Backspace)", "Haz clic en el hueco para escribir una ruta": "Click the empty space to type a path", "Actualizar (F5)": "Refresh (F5)", "Cortar": "Cut", "Mover a…": "Move to…", "Copiar a…": "Copy to…", "Más…": "More…", "Más acciones": "More actions", "Atajos: doble clic abre · clic derecho para el menú · Supr borra · F2 renombra · Ctrl+C / Ctrl+X / Ctrl+V · Ctrl+A selecciona todo · Retroceso sube": "Shortcuts: double-click opens · right-click for the menu · Del deletes · F2 renames · Ctrl+C / Ctrl+X / Ctrl+V · Ctrl+A selects all · Backspace goes up", "Mover aquí": "Move here", "Copiar aquí": "Copy here", "Nueva carpeta": "New folder", "Ya existe": "Already exists", "Reemplazar / combinar": "Replace / merge", "Reemplazar": "Replace", "Combinar": "Merge", "Conservar ambos": "Keep both", "Omitir": "Skip", "Reemplazar sobrescribe los archivos con el mismo nombre. Si es una carpeta, se combina: lo que ya tenía y no estás pegando se queda. Conservar ambos le pone (2) al nuevo.": "Replace overwrites files with the same name. Folders are merged: whatever they already had that you are not pasting stays. Keep both adds (2) to the new one.", "¿Qué hago con los que ya existen en la carpeta de destino?": "What should I do with the ones that already exist in the destination folder?", "Extraer": "Extract", "Extraer…": "Extract…", "Carpeta": "Folder", "Contraseña (solo si tiene)": "Password (only if it has one)", "La consola lo descomprime en una carpeta al lado del archivo (con BFpilot) y deja el comprimido como está. Nada pasa por el PC.": "The console extracts it into a folder next to the archive (with BFpilot) and leaves the archive as it is. Nothing goes through the PC.", "Notificaciones": "Notifications", "Marcar leídos": "Mark all read", "Borrar todo": "Clear all", "Descartar": "Dismiss", "Unidades": "Drives", "Accesos": "Places", "Recientes": "Recent", "Destinos recientes": "Recent destinations", "libres": "free", "Almacenamiento interno": "Internal storage", "Aplicaciones (homebrew)": "Apps (homebrew)", "Datos (/data)": "Data (/data)", "Paquetes (/data/pkg)": "Packages (/data/pkg)", "Restos de PS5 Directo": "PS5 Directo leftovers", "BFpilot no está cargado: el explorador va por FTP. Copiar pasa por el PC (más lento), y para descomprimir o ver el espacio libre hace falta BFpilot.": "BFpilot is not loaded: the browser works over FTP. Copying goes through the PC (slower), and extracting or showing free space needs BFpilot.", "desde": "from", "Pegar dentro": "Paste inside", "Pegar aquí": "Paste here", "Subir archivos aquí": "Upload files here", "Subir carpeta aquí": "Upload folder here", "Actualizar la carpeta": "Refresh folder", "Supr": "Del", "Copiar ruta de esta carpeta": "Copy this folder's path", "Abrir": "Open", "Descargar al PC": "Download to PC", "Copiar": "Copy", "Renombrar": "Rename", "Calcular tamaño": "Calculate size", "Permisos 777": "Permissions 777", "Copiar ruta": "Copy path", "Copiar rutas": "Copy paths", "Borrar": "Delete", "Ve a la carpeta de destino y pulsa \"Pegar aquí\" (o Ctrl+V)": "Go to the destination folder and click \"Paste here\" (or Ctrl+V)", "Ruta copiada": "Path copied", "El nombre no puede llevar / ni \\": "The name cannot contain / or \\", "No se puede pegar una carpeta dentro de sí misma": "You cannot paste a folder inside itself", "No hay carpetas dentro.": "No folders inside.", "Una carpeta no se puede meter dentro de sí misma.": "A folder cannot go inside itself.", "Ya está en esta carpeta.": "It is already in this folder.", "Otra unidad sin BFpilot: se copia pasando por el PC y luego se borra el original.": "Another drive without BFpilot: it is copied through the PC and then the original is deleted.", "BFpilot no está cargado: cárgalo en la PS5 para poder extraer.": "BFpilot is not loaded: load it on the PS5 to extract.", "Ya hay una carpeta con ese nombre: se usará uno nuevo terminado en (2).": "There is already a folder with that name: a new one ending in (2) will be used.", "Pon un solo nombre de carpeta, sin /": "Use a single folder name, without /", "No los encuentro en tu PC: usa \"Subir archivos aquí\"": "I cannot find them on your PC: use \"Upload files here\"", "Extrayendo": "Extracting", "ahora": "now", "Aún no hay avisos. Aquí aparecerán las transferencias terminadas, los errores, las desconexiones y las versiones nuevas.": "No notifications yet. Finished transfers, errors, disconnections and new versions will show up here.", "Novedades": "What's new", "Entendido": "Got it", "Se ha actualizado. Esto es lo nuevo:": "PS5 Directo has been updated. Here is what's new:", "Transferencias es la cola. En Archivos de la consola navegas por la PS5: ves tamaños, copias, mueves, renombras, borras, bajas al PC o subes archivos y carpetas. Espacio te enseña en un gráfico qué ocupa Line truncated
const I18N_RX=[["^Este ZIP usa cifrado AES y BFpilot todavia no sabe abrirlo \\(solo el cifrado ZIP clasico\\)\\. Los archivos ya estan en la PS5, en ([\\s\\S]*?)\\. Puedes bajarlos al PC, extraerlos con 7-Zip y subir la carpeta\\.$", "This ZIP uses AES encryption and BFpilot can't open it yet (only classic ZIP encryption). The files are already on the PS5, in {0}. You can download them to the PC, extract them with 7-Zip and upload the folder."], ["^Aviso: lo descomprimido puede ocupar hasta ~5 veces el RAR \\(~([\\s\\S]*?)\\) y quedan ([\\s\\S]*?) libres\\. Puede bastar, pero si el espacio baja del margen lo paro\\. Si puedes, libera espacio\\.$", "Warning: extracted content can take up to ~5 times the RAR (~{0}) and {1} is free. It may be enough, but if space drops below the margin I stop. Free up space if you can."], ["^Ojo: el contenido descomprimido puede ocupar hasta ~5 veces el RAR \\(~([\\s\\S]*?)\\) y no se si cabra\\. Si al descomprimir baja del margen, lo paro y los RAR se quedan en la PS5\\.$", "Heads up: extracted content can take up to ~5 times the RAR (~{0}) and it may not fit. If space drops below the margin while extracting, I stop and the RARs stay on the PS5."], ["^AVISO: no pude poner los permisos de ([\\s\\S]*?) \\(([\\s\\S]*?)\\)\\. Si no arranca, en FileZilla: clic derecho en la carpeta > Permisos > 777 > recurrir a subdirectorios\\.$", "WARNING: couldn't set permissions on {0} ({1}). If it doesn't launch, in FileZilla: right-click the folder > File permissions > 777 > recurse into subdirectories."], ["^No cabe en el PC: hay que bajar ([\\s\\S]*?) a ([\\s\\S]*?) y solo hay ([\\s\\S]*?) libres\\. Libera espacio, cambia la carpeta temporal en Ajustes o descomprime en la PS5\\.$", "Not enough space on the PC: {0} must be downloaded to {1} and only {2} is free. Free up space, change the temporary folder in Settings, or extract on the PS5."], ["^No cabe en la PS5: descomprimido ocupa ([\\s\\S]*?), faltan por mandar ([\\s\\S]*?), hay ([\\s\\S]*?) libres y dejo ([\\s\\S]*?) de margen\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: extracted it takes {0}, {1} left to send, {2} free and I keep {3} as margin. Free up space and click Retry."], ["^No hay sitio en la PS5: faltan por enviar ([\\s\\S]*?), hay ([\\s\\S]*?) libres y dejo ([\\s\\S]*?) de margen \\(Ajustes\\)\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: {0} left to send, {1} free and I keep {2} as margin (Settings). Free up space and click Retry."], ["^La IP de la PS5 no es valida \\(([\\s\\S]*?)\\): en Ajustes escribe solo los numeros, por ejemplo 192\\.168\\.1\\.50, sin ftp:\\/\\/ ni puerto$", "The PS5 IP is not valid ({0}): in Settings type just the numbers, for example 192.168.1.50, without ftp:// or a port"], ["^Parado para no llenar la PS5 \\(([\\s\\S]*?)\\): quedan ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar; sigue donde iba\\.$", "Stopped so the PS5 doesn't fill up ({0}): {1} free and the margin is {2}. Free up space and click Retry; it continues where it left off."], ["^BFpilot no abre ZIP partidos \\(\\.z01, \\.z02\\.\\.\\.\\)\\. Uno los RAR\\/7z partidos, pero este formato no\\. Los archivos estan en ([\\s\\S]*?)\\.$", "BFpilot can't open split ZIPs (.z01, .z02...). I can join split RAR/7z, but not this format. The files are in {0}."], ["^No cabe en la PS5: faltan ([\\s\\S]*?) por enviar, hay ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar\\.$", "It doesn't fit on the PS5: {0} left to send, {1} free and the margin is {2}. Free up space and click Retry."], ["^Ojo: ya tienes ([\\s\\S]*?) en ([\\s\\S]*?)\\. Dos copias con el mismo Title ID pueden liar a ShadowMountPlus \\(deja solo una\\)\\.([\\s\\S]*?)$", "Heads up: you already have {0} in {1}. Two copies with the same Title ID can confuse ShadowMountPlus (keep only one).{2}"], ["^Ojo: ya tienes ([\\s\\S]*?) en ([\\s\\S]*?)\\. Dos copias con el mismo Title ID pueden liar a ShadowMountPlus \\(deja solo una\\)\\.$", "Heads up: you already have {0} in {1}. Two copies with the same Title ID can confuse ShadowMountPlus (keep only one)."], ["^El comprimido tiene enlaces \\(([\\s\\S]*?)\\); eso solo lo sabe extraer BFpilot\\. Descomprimelo en la PS5\\.$", "The archive contains links ({0}); only BFpilot can extract those. Extract it on the PS5."], ["^No hay sitio en la PS5: quedan ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: {0} free and the margin is {1}. Free up space and click Retry."], ["^Ya existe ([\\s\\S]*?)\\. No lo piso: borralo o renombralo y pulsa Reintentar\\. Lo extraido esta en ([\\s\\S]*?)$", "{0} already exists. I won't overwrite it: delete or rename it and click Retry. What was extracted is in {1}"], ["^Recibido de la extensión: ([\\s\\S]*?)\\. Cuando estén todas las partes, pulsa \"Añadir a la cola\"\\.$", "Received from the extension: {0}. When all parts are iLine truncated
Object.assign(I18N_EXACT,{"Windows tiene bloqueado Python en el firewall (pasa si se cerró su aviso la primera vez).": "Windows Firewall is blocking Python (this happens if its prompt was closed the first time).", "Además, tu red está marcada como «Pública».": "Also, your network is marked as \"Public\".", "¿Se queda en blanco? Prueba de conexión": "Stays blank? Connection test", "Escanea este código pequeño. Si en el móvil sale «El móvil llega a PS5 Directo», la red está bien. Si no carga, el móvil no llega al PC: mira que esté en la misma Wi-Fi (no la de invitados ni con datos) y el firewall de abajo.": "Scan this small code. If your phone shows \"El móvil llega a PS5 Directo\", the network is fine. If it doesn't load, the phone can't reach the PC: check it's on the same Wi-Fi (not the guest network or mobile data) and the firewall below."});Object.assign(I18N_EXACT,{"Contraseña, destino, dónde descomprimir y el formato. Si no sabes qué elegir, déjalo como está: funciona así.": "Password, destination, where to extract and the format. If you're not sure what to pick, leave it as it is: it works like that."});Object.assign(I18N_EXACT,{"Cómo dejarlo en la PS5": "How to leave it on the PS5", "Opcional. Si no sabes cuál elegir, deja Carpeta.": "Optional. If you're not sure, leave Folder.", "Carpeta": "Folder", "Por defecto · sin convertir": "Default · no conversion", "Imagen .ffpkg": ".ffpkg image", "Recomendada": "Recommended", "Imagen .ffpfsc": ".ffpfsc image", "Ocupa menos": "Smaller", "Experimental": "Experimental", "LZ4 + .ffpkg": "LZ4 + .ffpkg", "El contenido se descomprime y se deja como una carpeta normal. Es lo de siempre: no se convierte nada y no ocupa sitio en el PC.": "The content is extracted and left as a normal folder. The usual way: nothing is converted and nothing takes up room on the PC.", "Imagen .ffpkg: un único archivo que ShadowMountPlus monta a velocidad completa. Es el formato que recomienda ShadowMountPlus. Viaja más rápido por FTP que miles de archivos pequeños y no tiene el problema de permisos.": ".ffpkg image: a single file ShadowMountPlus mounts at full speed. It's the format ShadowMountPlus recommends. It travels faster over FTP than thousands of small files and has no permissions problem.", "Imagen comprimida .ffpfsc: un contenedor que guarda dentro una imagen .ffpkg comprimida. Ocupa un 30-60 % menos en la consola, pero se lee más despacio (150-250 MB/s) y algunos títulos pueden dar tirones.": "Compressed .ffpfsc image: a container holding a compressed .ffpkg image inside. 30-60% smaller on the console, but read more slowly (150-250 MB/s) and some titles may stutter.", "LZ4 (carpeta): empaqueta los datos para que ocupen menos y carguen antes. Solo sirve para títulos AMPR; si no lo es, se manda como carpeta normal. Experimental: con algunos títulos se cuelga al guardar partida.": "LZ4 (folder): packs the data so it takes less room and loads faster. Only for AMPR titles; otherwise it's sent as a normal folder. Experimental: some titles crash when saving.", "LZ4 dentro de una imagen .ffpkg. Solo títulos AMPR; si no lo es, se manda como imagen .ffpkg normal. Experimental: con algunos títulos se cuelga al guardar partida.": "LZ4 inside an .ffpkg image. AMPR titles only; otherwise it's sent as a plain .ffpkg image. Experimental: some titles crash when saving.", "Se convierte en tu PC antes de enviarlo: mientras tanto ocupa espacio en el disco del PC (más o menos lo que ocupa descomprimido). Al terminar se borra.": "It's converted on your PC before sending: meanwhile it takes up room on the PC's disk (about its extracted size). It's deleted when done.", "Libre ahora en {0}: {1}": "Free now in {0}: {1}", "Antes de convertir": "Before converting", "Los enlaces se descargan primero al PC.": "Links are downloaded to the PC first.", "La conversión necesita sitio libre en el disco del PC (más o menos lo que ocupa el contenido descomprimido).": "The conversion needs free room on the PC's disk (about the content's extracted size).", "Al terminar de enviarse, se borra todo del PC.": "Once it's sent, everything is deleted from the PC.", "No volver a mostrar": "Don't show again", "Mejor carpeta normal": "Use normal folder instead", "Entendido": "Got it", "Con este formato, PS5 Directo prepara el contenido": "With this format, PS5 Directo prepares the content", "en tu PC": "on your PC", "antes de enviarlo, con PS5 Dump Forge. Hasta ahora todo pasaba directo a la consola sin ocupar sitio en el PC; con este formato no:": "before sending it, with PS5 Dump Forge. Until now everything went straight to the console without taking room on the PC; with this format it doesn't:"});Object.assign(I18N_EXACT,{"Windows tiene tu red marcada como «Pública»: así bloquea que el móvil entre.": "Windows has your network marked as \"Public\": that blocks your phone from connecting.", "Falta el permiso del Firewall de Windows para que el móvil pueda entrar.": "Windows FiLine truncated
Object.assign(I18N_EXACT,{"Windows tiene bloqueado Python en el firewall (pasa si se cerró su aviso la primera vez).": "Windows Firewall is blocking Python (this happens if its prompt was closed the first time).", "Además, tu red está marcada como «Pública».": "Also, your network is marked as \"Public\".", "¿Se queda en blanco? Prueba de conexión": "Stays blank? Connection test", "Escanea este código pequeño. Si en el móvil sale «El móvil llega a PS5 Directo», la red está bien. Si no carga, el móvil no llega al PC: mira que esté en la misma Wi-Fi (no la de invitados ni con datos) y el firewall de abajo.": "Scan this small code. If your phone shows \"El móvil llega a PS5 Directo\", the network is fine. If it doesn't load, the phone can't reach the PC: check it's on the same Wi-Fi (not the guest network or mobile data) and the firewall below."});Object.assign(I18N_EXACT,{"Contraseña, destino, dónde descomprimir y el formato. Si no sabes qué elegir, déjalo como está: funciona así.": "Password, destination, where to extract and the format. If you're not sure what to pick, leave it as it is: it works like that."});Object.assign(I18N_EXACT,{"Cómo dejarlo en la PS5": "How to leave it on the PS5", "Opcional. Si no sabes cuál elegir, deja Carpeta.": "Optional. If you're not sure, leave Folder.", "Carpeta": "Folder", "Por defecto · sin convertir": "Default · no conversion", "Imagen .ffpkg": ".ffpkg image", "Recomendada": "Recommended", "Imagen .ffpfsc": ".ffpfsc image", "Ocupa menos": "Smaller", "Experimental": "Experimental", "LZ4 + .ffpkg": "LZ4 + .ffpkg", "El contenido se descomprime y se deja como una carpeta normal. Es lo de siempre: no se convierte nada y no ocupa sitio en el PC.": "The content is extracted and left as a normal folder. The usual way: nothing is converted and nothing takes up room on the PC.", "Imagen .ffpkg: un único archivo que ShadowMountPlus monta a velocidad completa. Es el formato que recomienda ShadowMountPlus. Viaja más rápido por FTP que miles de archivos pequeños y no tiene el problema de permisos.": ".ffpkg image: a single file ShadowMountPlus mounts at full speed. It's the format ShadowMountPlus recommends. It travels faster over FTP than thousands of small files and has no permissions problem.", "Imagen comprimida .ffpfsc: un contenedor que guarda dentro una imagen .ffpkg comprimida. Ocupa un 30-60 % menos en la consola, pero se lee más despacio (150-250 MB/s) y algunos títulos pueden dar tirones.": "Compressed .ffpfsc image: a container holding a compressed .ffpkg image inside. 30-60% smaller on the console, but read more slowly (150-250 MB/s) and some titles may stutter.", "LZ4 (carpeta): empaqueta los datos para que ocupen menos y carguen antes. Solo sirve para títulos AMPR; si no lo es, se manda como carpeta normal. Experimental: con algunos títulos se cuelga al guardar partida.": "LZ4 (folder): packs the data so it takes less room and loads faster. Only for AMPR titles; otherwise it's sent as a normal folder. Experimental: some titles crash when saving.", "LZ4 dentro de una imagen .ffpkg. Solo títulos AMPR; si no lo es, se manda como imagen .ffpkg normal. Experimental: con algunos títulos se cuelga al guardar partida.": "LZ4 inside an .ffpkg image. AMPR titles only; otherwise it's sent as a plain .ffpkg image. Experimental: some titles crash when saving.", "Se convierte en tu PC antes de enviarlo: mientras tanto ocupa espacio en el disco del PC (más o menos lo que ocupa descomprimido). Al terminar se borra.": "It's converted on your PC before sending: meanwhile it takes up room on the PC's disk (about its extracted size). It's deleted when done.", "Libre ahora en {0}: {1}": "Free now in {0}: {1}", "Antes de convertir": "Before converting", "Los enlaces se descargan primero al PC.": "Links are downloaded to the PC first.", "La conversión necesita sitio libre en el disco del PC (más o menos lo que ocupa el contenido descomprimido).": "The conversion needs free room on the PC's disk (about the content's extracted size).", "Al terminar de enviarse, se borra todo del PC.": "Once it's sent, everything is deleted from the PC.", "No volver a mostrar": "Don't show again", "Mejor carpeta normal": "Use normal folder instead", "Entendido": "Got it", "Con este formato, PS5 Directo prepara el contenido": "With this format, PS5 Directo prepares the content", "en tu PC": "on your PC", "antes de enviarlo, con PS5 Dump Forge. Hasta ahora todo pasaba directo a la consola sin ocupar sitio en el PC; con este formato no:": "before sending it, with PS5 Dump Forge. Until now everything went straight to the console without taking room on the PC; with this format it doesn't:"});Object.assign(I18N_EXACT,{"Windows tiene tu red marcada como «Pública»: así bloquea que el móvil entre.": "Windows has your network marked as \"Public\": that blocks your phone from connecting.", "Falta el permiso del Firewall de Windows para que el móvil pueda entrar.": "Windows FiLine truncated
Object.assign(I18N_EXACT,{"Sin convertir": "No conversion", "tal cual": "as is", "Por defecto": "Default", "Por defecto.": "Default.", "Recomendada.": "Recommended.", "Ocupa menos.": "Smaller.", "Experimental.": "Experimental.", "Imagen": "Image", "Imagen comprimida": "Compressed image", "LZ4 en imagen": "LZ4 in an image", "carpeta": "folder", "Opcional. Si no sabes cuál elegir, deja «Sin convertir».": "Optional. If you're not sure, leave \"No conversion\".", "Mejor sin convertir": "Don't convert", "Sin convertir (tal cual)": "No conversion (as is)", "Se deja tal como viene: si es un comprimido se descomprime, y si ya es una imagen (.ffpkg, .exfat, .ffpfsc) o viene empaquetado en LZ4, se envía sin tocar. No ocupa sitio en el PC.": "Left as it comes: archives are extracted, and if it's already an image (.ffpkg, .exfat, .ffpfsc) or LZ4-packed, it's sent untouched. Takes no room on the PC.", "Imagen .ffpkg (UFS2): ojo, no es un paquete .pkg/.fpkg de instalar; es una imagen de disco que ShadowMountPlus monta a velocidad completa. Viaja más rápido por FTP que miles de archivos pequeños y no tiene el problema de permisos.": ".ffpkg image (UFS2): note it isn't an installable .pkg/.fpkg package; it's a disk image ShadowMountPlus mounts at full speed. It travels faster over FTP than thousands of small files and has no permissions problem.", "Imagen comprimida .ffpfsc: un contenedor que guarda dentro una imagen .ffpkg comprimida. Ocupa un 30-60 % menos en la consola, pero se lee más despacio (150-250 MB/s) y algunos títulos pueden dar tirones.": "Compressed .ffpfsc image: a container holding a compressed .ffpkg image inside. 30-60% smaller on the console, but read more slowly (150-250 MB/s) and some titles may stutter.", "LZ4 (carpeta): empaqueta los datos para que ocupen menos y carguen antes. Solo sirve para títulos AMPR; si no lo es, se manda sin convertir. Con algunos títulos se cuelga al guardar partida.": "LZ4 (folder): packs the data so it takes less room and loads faster. Only for AMPR titles; otherwise it's sent without conversion. Some titles crash when saving.", "LZ4 dentro de una imagen .ffpkg. Solo títulos AMPR; si no lo es, se manda como imagen .ffpkg normal. Con algunos títulos se cuelga al guardar partida.": "LZ4 inside an .ffpkg image. AMPR titles only; otherwise it's sent as a plain .ffpkg image. Some titles crash when saving."});
Object.assign(I18N_EXACT,{"Página principal": "Main page", "Página de prueba": "Test page", "Actualizaciones de la cola": "Queue updates", "Qué le llega del móvil": "What arrives from the phone", "Todavía nada. Si ya has escaneado el código y no aparece nada aquí, el móvil no está llegando al PC: comprueba que está en la misma Wi-Fi (no la de invitados) y con los datos móviles apagados.": "Nothing yet. If you've scanned the code and nothing shows up here, the phone isn't reaching the PC: check it's on the same Wi-Fi (not the guest one) with mobile data off.", "La página empezó a enviarse pero se cortó. Suele ser un antivirus, una VPN o un «escudo web» del PC. Desactívalo un momento y vuelve a escanear.": "The page started sending but was cut off. That's usually an antivirus, a VPN or a \"web shield\" on the PC. Turn it off for a moment and scan again.", "El móvil pidió la página y el PC se la envió entera. Si se ve en blanco, escanea el código pequeño de prueba: dice si la recibe completa.": "The phone asked for the page and the PC sent all of it. If it looks blank, scan the small test code: it tells you whether it arrives complete."});
Object.assign(I18N_EXACT,{"Carpeta": "Folder", "normal": "plain", "Sin imagen": "No image", "Compatible": "Compatible", "Imagen PFS": "PFS image", "Formato PS5": "PS5 format", "Paquete": "Package", "FW ≤ 11.60": "FW ≤ 11.60", "No en tu firmware": "Not on your FW", "Compresión": "Compression", "Rápida": "Fast", "Equilibrada": "Balanced", "Mínimo tamaño": "Smallest", "Sin comprimir": "Uncompressed", "Carpeta normal.": "Plain folder.", "Compatible.": "Compatible.", "Formato PS5.": "PS5 format.", "Solo firmware 11.60 o inferior.": "Firmware 11.60 or lower only.", "Carpeta normal": "Plain folder", "Imagen .exfat": ".exfat image", "Imagen .ffpfs": ".ffpfs image", "Paquete .fpkg": ".fpkg package", "Se deja tal como viene: si es un comprimido se descomprime, y si ya es una imagen (.ffpkg, .exfat, .ffpfs, .ffpfsc), un .fpkg o viene empaquetado en LZ4, se envía sin tocar. No ocupa sitio en el PC.": "Left as it comes: archives are extracted, and if it's already an image (.ffpkg, .exfat, .ffpfs, .ffpfsc), an .fpkg or LZ4-packed, it's sent untouched. Takes no room on the PC.", "Siempre acaba como una carpeta de archivos sueltos: un comprimido se descomprime como siempre, y si es una imagen, un .fpkg o viene empaquetado en LZ4, se deshace en el PC. Útil para cargadores que solo leen carpetas.": "Always ends up as a folder of loose files: archives are extracted as usual, and images, .fpkg or LZ4-packed content are unpacked on the PC. Handy for loaders that only read folders.", "Imagen .exfat: un único archivo que ShadowMountPlus también monta. Útil si otra herramienta o un USB te la pide en exFAT; si no, mejor .ffpkg.": ".exfat image: a single file ShadowMountPlus also mounts. Useful if another tool or a USB setup needs exFAT; otherwise .ffpkg is better.", "Imagen .ffpfs (PFS, el sistema de archivos de la propia PS5). ShadowMountPlus la monta; ocupa lo mismo que la carpeta.": ".ffpfs image (PFS, the PS5's own file system). ShadowMountPlus mounts it; same size as the folder.", "Paquete .fpkg: se instala como una aplicación de la tienda y va a velocidad nativa. Necesita kstuff, fpkg-enable y ppr-patch. Se deja en /data/pkg para instalarlo con el instalador de paquetes.": ".fpkg package: installs like a store app and runs at native speed. Needs kstuff, fpkg-enable and ppr-patch. It's left in /data/pkg to install with the package installer.", "Recomendado. La creación más rápida.": "Recommended. Fastest to create.", "Un 2-3 % más pequeño, pero tarda unas 6 veces más.": "2-3% smaller, but takes about 6x longer.", "Lo más pequeño posible; tarda unas 9 veces más.": "As small as possible; takes about 9x longer.", "Sin comprimir: mucho más grande. Solo si tu consola no arranca los comprimidos.": "Uncompressed: much bigger. Only if your console won't start compressed ones.", "Se crea antes, ocupa algo más.": "Faster to create, a bit bigger.", "Recomendado. Buen equilibrio entre tamaño y tiempo.": "Recommended. Good balance of size and time.", "Lo más pequeño posible; tarda bastante más.": "As small as possible; takes much longer.", "Sin comprimir: ocupa como un .ffpfs normal.": "Uncompressed: same size as a plain .ffpfs.", "Si es una imagen, un .fpkg o LZ4, se deshace en tu PC antes de enviarlo y mientras tanto ocupa espacio en el disco del PC. Un comprimido normal no ocupa nada extra.": "Images, .fpkg or LZ4 are unpacked on your PC first and take PC disk space meanwhile. A normal archive takes nothing extra.", "Tu PS5 tiene firmware {0}: este formato no te funcionará. Elige «Imagen .ffpkg».": "Your PS5 is on firmware {0}: this format won't work for you. Pick \".ffpkg image\"."});
@@ -1043,6 +1059,14 @@ Object.assign(I18N_EXACT,{"Comprobar arranque": "Launch check", "Comprobar": "Ch
I18N_RX.push(['^Pide firmware ([\\d.]+) y tu PS5 tiene ([\\d.]+)\\.$','Needs firmware {0} and your PS5 has {1}.'],['^Firmware: pide ([\\d.]+) y tu PS5 tiene ([\\d.]+)\\.$','Firmware: needs {0} and your PS5 has {1}.'],['^LZ4: tiene el índice de paquetes \\((\\d+) archivos de datos\\)\\.$','LZ4: has the pack index ({0} data files).']);
Object.assign(I18N_EXACT,{"Esa copia ya no está en la carpeta de copias (¿la moviste o borraste?). Pulsa Actualizar en la lista de copias.": "That backup is no longer in the backups folder (did you move or delete it?). Click Refresh in the backups list."});
Object.assign(I18N_EXACT,{"🍎 Versión de Mac en pruebas:": "🍎 Mac version in testing:", "todavía no se ha podido probar en un Mac de verdad. Si algo falla, pulsa «Crear informe de fallo» y adjúntalo en un aviso en GitHub (Issues): con eso se arregla rápido. ¡Gracias por probarla!": "it hasn't been tested on a real Mac yet. If something fails, click \"Create problem report\" and attach it to an issue on GitHub: that makes it quick to fix. Thanks for trying it!", "Para bajar torrents hace falta aria2. Instálalo con Homebrew (brew.sh): brew install aria2": "Torrents need aria2. Install it with Homebrew (brew.sh): brew install aria2"});
Object.assign(I18N_EXACT,{"PS5 Dump Forge dice que este título no usa AMPR, así que no se puede pasar a LZ4: se envía sin convertir, como carpeta normal.": "PS5 Dump Forge says this title doesn't use AMPR, so it can't be turned into LZ4: it's sent unconverted, as a plain folder.", "Este título no usa AMPR, así que LZ4 no sirve con él": "This title doesn't use AMPR, so LZ4 doesn't work with it"});
I18N_RX.push(['^PS5 Dump Forge dice que este título no usa AMPR, así que no se puede pasar a LZ4: se envía sin convertir, tal cual \\(([\\s\\S]*?)\\)\\.$','PS5 Dump Forge says this title doesn\'t use AMPR, so it can\'t be turned into LZ4: it\'s sent unconverted, as is ({0}).']);
Object.assign(I18N_EXACT,{"Bajar al PC": "Download to PC", "Copia este título a la carpeta de PS5 Directo en tu PC (sale luego en «En tu PC»)": "Copies this title to the PS5 Directo folder on your PC (it then shows up in \"On your PC\")", "¿Bajar al PC?": "Download to the PC?", "Bajando al PC:": "Downloading to PC:", "Bajado al PC:": "Downloaded to PC:", "Lo tienes en «En tu PC».": "You'll find it in \"On your PC\".", "Carpeta de PS5 Directo (aquí llega lo que bajas de la consola):": "PS5 Directo folder (what you download from the console lands here):", "Cambiar…": "Change…", "Por defecto": "Default", "Carpeta donde guardar lo que bajas de la consola": "Folder to save what you download from the console", "Ruta de la carpeta (por ejemplo D:\\PS5):": "Folder path (for example D:\\PS5):", "No has añadido otras carpetas.": "You haven't added other folders.", "de": "of", "Lo que tienes en tu PC o en tu NAS para la consola. Te marca lo que ya está en la consola y lo que no, y lo envías con un clic (con el formato y el destino elegidos en Transferencias). Lo que bajas de la consola con «Bajar al PC» en la Biblioteca llega a la carpeta de PS5 Directo y sale aquí solo. Puedes añadir otras carpetas tuyas (también valen rutas de red como \\\\NAS\\carpeta).": "What you have on your PC or NAS for the console. It marks what is already on the console and what isn't, and you send it with one click (with the format and destination chosen in Transfers). What you download from the console with \"Download to PC\" in the Library lands in the PS5 Directo folder and shows up here automatically. You can add other folders of yours (network paths like \\\\NAS\\folder work too).", "Pon una ruta completa, por ejemplo D:\\PS5": "Enter a full path, for example D:\\PS5", "No hay nada que bajar": "There's nothing to download"});
I18N_RX.push(['^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\\S]*?)\\)$','No connection to the console. Check that the PS5 is on, that the FTP payload is loaded (the «FTP» dot at the top must be green) and click Refresh. ({0})']);
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"});
// --- 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'})();
@@ -1155,7 +1179,7 @@ function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt
const ORDER=['comprobando','enviando','en cola para descomprimir','extrayendo','colocando','hecho'];
const FMT_SHORT={plain:'carpeta',ffpkg:'.ffpkg',exfat:'.exfat',ffpfs:'.ffpfs',ffpfsc:'.ffpfsc',fpkg:'.fpkg',lz4:'LZ4',lz4ffpkg:'LZ4 en .ffpkg'};
function convLabel(j){const f=j.fmt_real||j.fmt; return FMT_SHORT[f]?'Convertir a '+FMT_SHORT[f]:'Convertir'}
function convLabel(j){const f=j.fmt_real||j.fmt; if(f==='none') return 'Sin convertir'; return FMT_SHORT[f]?'Convertir a '+FMT_SHORT[f]:'Convertir'}
function pcStepsHtml(j){
// Modo "descomprimir en el PC": bajar al PC -> descomprimir y enviar -> colocar -> listo
// Con PS5 Dump Forge: bajar al PC -> convertir -> enviar -> listo
@@ -1752,7 +1776,17 @@ async function mdLoad(){
try{const r=await api('/api/media/list',{}); if(!r.ok) throw new Error(r.error); md.items=r.items; md.sel.clear(); mdPaint()}catch(e){$('mdSum').textContent=e.message}
}
function mdWhen(w){ try{ return new Date(w).toLocaleString(LANG==='en'?'en-GB':'es-ES',{dateStyle:'short',timeStyle:'short'}) }catch(e){ return w } }
function mdTipsPaint(){
let hid={}; try{hid=JSON.parse(localStorage.getItem('ps5d_md_tips')||'{}')}catch(e){}
const webm=md.items.some(x=>/\.webm$/i.test(x.name)), any=md.items.length>0;
const tips=[];
if(webm&&!hid.mp4) tips.push(['mp4','🎬 '+tr('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).')]);
if(any&&!hid.hdr) tips.push(['hdr','🌗 '+tr('¿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.')]);
$('mdTips').innerHTML=tips.map(([k,t])=>`<div class="md-tip"><div>${esc(t)}</div><button class="small" data-tip="${k}">${esc(tr('Entendido'))}</button></div>`).join('');
$('mdTips').querySelectorAll('[data-tip]').forEach(b=>b.onclick=()=>{hid[b.dataset.tip]=1; try{localStorage.setItem('ps5d_md_tips',JSON.stringify(hid))}catch(e){} mdTipsPaint()});
}
function mdPaint(){
mdTipsPaint();
const n=md.items.length, v=md.items.filter(x=>x.video).length;
$('mdSum').textContent=n?tr('{0} capturas y {1} vídeos').replace('{0}',n-v).replace('{1}',v)+' · '+human(md.items.reduce((a,x)=>a+(x.size||0),0)):tr('No hay capturas ni vídeos en la consola.');
$('mdGrid').innerHTML=md.items.map((x,i)=>{const u=`/api/media/file?p=${encodeURIComponent(x.path)}&t=${encodeURIComponent(TOKEN)}&v=1`;
@@ -1775,15 +1809,22 @@ let col={roots:[],items:null,poll:null};
$('libView').addEventListener('click',e=>{const b=e.target.closest('button[data-v]'); if(!b) return;
$('libView').querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b));
const pc=b.dataset.v==='pc'; $('libCard').hidden=pc; $('colCard').hidden=!pc; if(pc) colEnter();});
async function colEnter(){ await colState(); if(col.items===null&&col.roots.length) colScanNow(); if(!lib.res) libScan(false); }
async function colEnter(){ await colState(); if(col.items===null) colScanNow(); if(!lib.res) libScan(false); }
async function colState(){
let r; try{r=await api('/api/col/state',{})}catch(e){return}
col.roots=r.roots||[]; col.items=r.col.items; colPaintRoots();
col.roots=r.roots||[]; col.libdir=r.libdir; col.libdirCustom=!!r.libdir_custom; col.items=r.col.items; colPaintRoots();
if(r.col.busy){ $('colSum').textContent=tr('Leyendo: {0}').replace('{0}',r.col.current||''); if(!col.poll) col.poll=setInterval(colState,800) }
else { clearInterval(col.poll); col.poll=null; colPaint(); if(r.col.error) toast(r.col.error) }
}
function colPaintRoots(){
$('colRoots').innerHTML=col.roots.length?col.roots.map((r,i)=>`<span class="colroot"><span data-noi18n class="mono">${esc(r)}</span><button title="${esc(tr('Quitar'))}" data-rm="${i}">✕</button></span>`).join(''):`<span class="hint">${esc(tr('Aún no has elegido ninguna carpeta.'))}</span>`;
$('colLib').innerHTML=`<span>📥 ${esc(tr('Carpeta de PS5 Directo (aquí llega lo que bajas de la consola):'))}</span><span data-noi18n class="mono">${esc(col.libdir||'')}</span><div class="spacer"></div>`+
`<button class="small" id="colLibOpen">${esc(tr('Abrir'))}</button><button class="small" id="colLibChg">${esc(tr('Cambiar…'))}</button>`+(col.libdirCustom?`<button class="small" id="colLibDef">${esc(tr('Por defecto'))}</button>`:'');
$('colLibOpen').onclick=async()=>{try{const r=await api('/api/col/openlibdir',{}); if(!r.ok) throw new Error(r.error)}catch(e){toast(e.message)}};
$('colLibChg').onclick=async()=>{ let paths=[]; try{const r=await api('/api/pick',{kind:'folder',title:tr('Carpeta donde guardar lo que bajas de la consola')}); paths=r.paths||[]}catch(e){}
if(!paths.length){ const p=prompt(tr('Ruta de la carpeta (por ejemplo D:\\PS5):'),col.libdir||''); if(p) paths=[p] }
if(!paths.length) return; try{const r=await api('/api/col/libdir',{path:paths[0]}); if(!r.ok) throw new Error(r.error); col.items=null; colEnter()}catch(e){toast(e.message)} };
if($('colLibDef')) $('colLibDef').onclick=async()=>{ await api('/api/col/libdir',{path:''}); col.items=null; colEnter() };
$('colRoots').innerHTML=col.roots.length?col.roots.map((r,i)=>`<span class="colroot"><span data-noi18n class="mono">${esc(r)}</span><button title="${esc(tr('Quitar'))}" data-rm="${i}">✕</button></span>`).join(''):`<span class="hint">${esc(tr('No has añadido otras carpetas.'))}</span>`;
$('colRoots').querySelectorAll('[data-rm]').forEach(b=>b.onclick=async()=>{const roots=col.roots.filter((_,i)=>i!==+b.dataset.rm); await api('/api/col/set',{roots}); col.items=null; colEnter()});
}
async function colScanNow(){ try{await api('/api/col/scan',{}); setTimeout(colState,300)}catch(e){toast(e.message)} }
@@ -1845,7 +1886,6 @@ function showTab(t){
curTab=t;
['Transfers','Library','Files','Space','Backup','Media','Activity'].forEach(x=>{$('tab'+x).hidden=(x!==t)});
document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.tab===t));
try{localStorage.setItem('ps5d_tab',t)}catch(e){}
// lo que carga cada pestaña: si algo falla, la pestaña ya está bien cambiada
try{
if(t==='Backup') bkEnter();
@@ -2427,9 +2467,7 @@ $('actBody').addEventListener('click',async e=>{const b=e.target.closest('button
if(!confirm(tr('¿Empezar de nuevo desde cero? Se vuelve a descargar y enviar, como si pegaras el enlace otra vez.'))) return;
try{await api('/api/history/redo',{id:b.dataset.redo}); toast(tr('Añadido a la cola')); showTab('Transfers'); refresh()}catch(err){toast(err.message)}});
$('actClear').onclick=async()=>{if(!confirm(tr('¿Borrar todo el historial?')))return; await api('/api/history/clear',{}); loadActivity()};
// vuelve a la última pestaña cuando ya está todo el programa cargado (antes la Biblioteca fallaba al
// abrir y se quedaba marcada «Transferencias» con el contenido de la Biblioteca)
setTimeout(()=>{try{const t=localStorage.getItem('ps5d_tab'); if(t&&t!=='Transfers'&&$('tab'+t)) showTab(t)}catch(e){console.error(e)}},0);
// El programa se abre siempre en «Transferencias» (antes volvía a la última pestaña usada y confundía).
</script>
<script>
// ===================== 1.20: formato, programar, payloads, diagnóstico, móvil =====================
@@ -2557,24 +2595,57 @@ $('scSave').onclick=async()=>{try{await api('/api/schedule',{start:$('scStart').
$('scClear').onclick=async()=>{try{await api('/api/schedule',{start:'',after:'none'});$('dlgSched').close();refresh()}catch(e){toast(e.message)}};
// --- payloads
let plSt=null;
async function openPayloads(highlight){
try{$('dlg').open&&$('dlg').close()}catch(e){}
$('plOut').textContent=''; $('plList').innerHTML='<p class="hint">'+esc(tr('Cargando…'))+'</p>'; $('dlgPayloads').showModal();
let r; try{r=await api('/api/payload/list',{})}catch(e){$('plList').textContent=e.message;return}
$('plPort').textContent=r.elf_port;
$('plList').innerHTML=r.items.map(p=>`<div class="pl-row" data-id="${esc(p.id)}"><div><b data-noi18n>${esc(p.name)}</b><div class="hint">${esc(tr(p.what))}</div>
<div class="hint">${p.cached?esc(tr('Guardado: {0}').replace('{0}',p.cached)):esc(tr('Se descargará de GitHub'))} · <span data-noi18n>${esc(p.repo)}</span></div></div>
<button class="small ${p.id===highlight?'primary':''}" data-send="${esc(p.id)}">${esc(tr('Enviar a la PS5'))}</button></div>`).join('');
$('plOut').textContent=''; if(!$('dlgPayloads').open) $('dlgPayloads').showModal();
$('plPm').innerHTML='<p class="hint">'+esc(tr('Mirando la consola…'))+'</p>'; $('plList').innerHTML=''; $('plAuto').innerHTML='';
$('plAutoStart').checked=!state.settings||state.settings.auto_payloads!==false;
let r; try{r=await api('/api/console/status',{}); if(!r.ok) throw new Error(r.error)}catch(e){$('plPm').textContent=e.message;return}
plSt=r; plPaint(highlight);
}
function plPaint(highlight){
const r=plSt, pm=r.pm||{};
$('plPm').innerHTML=!r.reachable
?`<div class="pl-box">✖ ${esc(tr('No encuentro la consola en {0}. ¿Está encendida y con el jailbreak hecho? Revisa la IP en Ajustes.').replace('{0}',r.ip||'?'))}</div>`
:pm.present?`<div class="pl-box">✔ ${esc(tr('Payload Manager está abierto en la consola: PS5 Directo puede iniciar, instalar y reiniciar los payloads desde aquí.'))}</div>`
:`<div class="pl-box">⚠ ${esc(tr('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.'))}</div>`;
$('plList').innerHTML=r.items.map(it=>{
const st=it.ok?'ok':it.state; const lab={ok:tr('Funciona'),off:tr('No está iniciado'),stuck:tr('En marcha, pero no responde')}[st];
let btn='';
if(!it.ok&&pm.present){
if(it.state==='stuck') btn=`<button class="small primary" data-fix="restart" data-id="${esc(it.id)}">${esc(tr('Reiniciar'))}</button>`;
else if(it.installed) btn=`<button class="small primary" data-fix="start" data-id="${esc(it.id)}">${esc(tr('Iniciar'))}</button>`;
else btn=`<button class="small primary" data-fix="install" data-id="${esc(it.id)}">${esc(tr('Instalar e iniciar'))}</button>`;
} else if(!it.ok&&r.loader) btn=`<button class="small primary" data-fix="start" data-id="${esc(it.id)}">${esc(tr('Enviar a la PS5'))}</button>`;
const why=it.ok?'':it.state==='stuck'?tr('Está en marcha pero no contesta: se ha colgado o está cargado dos veces. «Reiniciar» lo cierra y lo vuelve a iniciar.')
:(pm.present&&!it.installed?tr('No está instalado en Payload Manager: «Instalar e iniciar» baja la última versión de GitHub, la instala allí y la inicia.'):'');
return `<div class="pl-row ${it.id===highlight?'hl':''}"><div><b data-noi18n>${esc(it.name)}</b><span class="pl-st ${st}">${esc(lab)}</span>
<div class="hint">${esc(tr(it.id==='ftpsrv'?'Servidor FTP (puerto 2121): por él se envía todo':'Descomprime, mueve y borra en la consola (puerto 5905)'))}</div>
${why?`<div class="hint">${esc(why)}</div>`:''}${it.ok?'':`<div class="hint pl-lost">${esc(tr(it.lost))}</div>`}</div>${btn}</div>`}).join('');
const inst=r.items.filter(i=>i.installed), inAuto=inst.filter(i=>i.autoload);
$('plAuto').innerHTML=!pm.present?'':inst.length&&inAuto.length===r.items.length
?`<p class="hint">✔ ${esc(tr('ftpsrv y BFpilot se cargan solos cada vez que se abre Payload Manager.'))}</p>`
:inst.length?`<div class="pl-box">${esc(tr('¿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.'))}<div class="actions"><button class="small" data-fix="autoload" data-id="">${esc(tr('Cargarlos solos al encender'))}</button></div></div>`:'';
$('plFile').hidden=!!state.remote;
}
$('plList').addEventListener('click',async e=>{
const b=e.target.closest('[data-send]'); if(!b) return; const o=$('plOut');
b.disabled=true; o.textContent=tr('Enviando… (descarga, envío y espera a que arranque: unos segundos)');
try{const r=await api('/api/payload/send',{id:b.dataset.send});
o.textContent=r.up===false?tr('{0} enviado, pero todavía no responde. Espera unos segundos y pulsa Probar conexión.').replace('{0}',r.name+(r.tag?' '+r.tag:'')):tr('{0} cargado en la PS5.').replace('{0}',r.name+(r.tag?' '+r.tag:''));
checkConn();
}catch(err){o.textContent=err.message} finally{b.disabled=false}
});
async function plFix(b){
const o=$('plOut'); const a=b.dataset.fix;
document.querySelectorAll('#dlgPayloads [data-fix]').forEach(x=>x.disabled=true);
o.textContent=tr({start:'Iniciando… (unos segundos)',install:'Bajando de GitHub, instalando en Payload Manager e iniciando…',restart:'Cerrando el que estaba colgado y volviendo a iniciarlo…',autoload:'Guardando en Payload Manager…'}[a]);
try{const r=await api('/api/console/fix',{id:b.dataset.id,action:a}); if(!r.ok) throw new Error(r.error);
const msg=a==='autoload'?tr('Listo: se cargarán solos al encender la consola.'):r.up===false?tr('{0} iniciado, pero todavía no responde. Espera unos segundos y pulsa «Volver a mirar».').replace('{0}',r.name):tr('{0} funcionando.').replace('{0}',r.name);
checkConn(); await openPayloads(); o.textContent='✔ '+msg;
}catch(err){o.textContent=err.message; document.querySelectorAll('#dlgPayloads [data-fix]').forEach(x=>x.disabled=false)}
}
$('dlgPayloads').addEventListener('click',e=>{const b=e.target.closest('[data-fix]'); if(b) plFix(b)});
$('plRefresh').onclick=()=>openPayloads();
$('plAutoStart').onchange=async()=>{try{await api('/api/settings',{auto_payloads:$('plAutoStart').checked}); if(state.settings) state.settings.auto_payloads=$('plAutoStart').checked}catch(e){toast(e.message)}};
// al abrir el programa: si la consola responde pero falta algo, se enseña esta ventana (una vez)
setTimeout(async()=>{ if(state.remote) return; let r; try{r=await api('/api/console/status',{})}catch(e){return}
if(!r.ok||!r.reachable||r.items.every(i=>i.ok)) return;
if(document.querySelector('dialog[open]')) return;
openPayloads(); }, 25000);
$('plFile').onclick=async()=>{
const ps=await pick('files'); if(!ps.length) return; const o=$('plOut'); o.textContent=tr('Enviando…');
try{const r=await api('/api/payload/file',{path:ps[0]}); o.textContent=tr('{0} enviado a la PS5.').replace('{0}',r.name)}catch(e){o.textContent=e.message}
@@ -2739,7 +2810,7 @@ function libRender(){
<div class="gmeta" data-noi18n title="${esc(it.path)}">${esc(meta)}${it.size?' · '+esc(human(it.size)):''}</div>
<div class="badges">${b.join('')}</div>
${/^PPSA\d{5}$/i.test(it.tid||'')&&!it.extra&&!(it.need_fw&&parseFloat(it.need_fw)===0)?`<a class="gupd" href="https://prosperopatches.com/${esc(it.tid.toUpperCase())}" target="_blank" rel="noopener noreferrer" title="${esc(updHint())}">↗ ${esc(tr('Ver actualizaciones'))}</a>`:''}
<div class="gact">${it.kind!=='installed'&&!it.incomplete?`<button data-g="check" title="${esc(tr('Si no abre o se cierra al arrancar: revisa la carpeta y busca registros y capturas'))}">${esc(tr('Comprobar'))}</button>`:''}${it.kind==='installed'&&!/^\/user\/app\//.test(it.path)?'':`<button data-g="open">${esc(tr('Abrir'))}</button>`}${it.kind==='folder'&&!it.incomplete?`<button data-g="perm" title="${esc(tr('Permisos 777 (si da CE-107750-0)'))}">${esc(tr('Permisos'))}</button>`:''}${it.kind==='installed'?`<span class="hint" title="${esc(tr('Instalado por la consola: se desinstala desde su menú, no desde aquí.'))}">${esc(tr('Se gestiona desde la consola'))}</span>`:`<button class="danger" data-g="del">${esc(tr('Borrar'))}</button>`}</div></div></div>`;
<div class="gact">${it.kind!=='installed'&&!it.incomplete?`<button data-g="check" title="${esc(tr('Si no abre o se cierra al arrancar: revisa la carpeta y busca registros y capturas'))}">${esc(tr('Comprobar'))}</button>`:''}${it.kind==='installed'&&!/^\/user\/app\//.test(it.path)?'':`<button data-g="open">${esc(tr('Abrir'))}</button>`}${(it.kind==='folder'||it.kind==='image')&&!it.incomplete&&/^\/(data|mnt)\//.test(it.path)?`<button data-g="dl" title="${esc(tr('Copia este título a la carpeta de PS5 Directo en tu PC (sale luego en «En tu PC»)'))}">${esc(tr('Bajar al PC'))}</button>`:''}${it.kind==='folder'&&!it.incomplete?`<button data-g="perm" title="${esc(tr('Permisos 777 (si da CE-107750-0)'))}">${esc(tr('Permisos'))}</button>`:''}${it.kind==='installed'?`<span class="hint" title="${esc(tr('Instalado por la consola: se desinstala desde su menú, no desde aquí.'))}">${esc(tr('Se gestiona desde la consola'))}</span>`:`<button class="danger" data-g="del">${esc(tr('Borrar'))}</button>`}</div></div></div>`;
}).join('');
}
function updHint(){
@@ -2752,6 +2823,8 @@ $('libGrid').addEventListener('click',async e=>{
const btn=e.target.closest('[data-g]'); if(!btn) return; const it=libItems()[+btn.closest('.gcard').dataset.i]; if(!it) return;
const a=btn.dataset.g;
if(a==='check'){openLaunch(it); return}
if(a==='dl'){ if(!confirm(tr('¿Bajar al PC?')+'\n\n'+(it.title?it.title+' — ':'')+it.path+(it.size?' ('+human(it.size)+')':''))) return;
try{const r=await api('/api/lib/download',{paths:[it.path]}); if(!r.ok) throw new Error(r.error); libDlWatch(it.title||it.name)}catch(err){toast(err.message)} return}
if(a==='open'){showTab('Files'); fmGo(it.kind==='folder'?it.path:parentOf(it.path)); return}
if(a==='perm'){try{const r=await api('/api/fm/chmod',{paths:[it.path]}); if(!r.ok) throw new Error(r.error); toast(tr('Poniendo permisos 777…'))}catch(err){toast(err.message)} return}
if(a==='del'){
@@ -2762,6 +2835,23 @@ $('libGrid').addEventListener('click',async e=>{
}catch(err){toast(err.message)}
}
});
// descarga al PC desde la Biblioteca: progreso aquí mismo (es la misma operación del explorador)
let libDl={t:null,name:''};
function libDlWatch(name){ libDl.name=name||libDl.name; clearInterval(libDl.t); libDl.t=setInterval(libDlPoll,1000); libDlPoll(); }
async function libDlPoll(){
let o; try{o=(await api('/api/fm/op',{})).op}catch(e){return}
const box=$('libDl');
if(o.op!=='download'){ clearInterval(libDl.t); box.hidden=true; return }
box.hidden=false;
if(o.busy){ const pct=o.bytes_total?Math.floor(o.bytes_done*100/o.bytes_total):0;
box.innerHTML=`<span>⬇ ${esc(tr('Bajando al PC:'))} <b data-noi18n>${esc(libDl.name)}</b> · ${pct}% (${human(o.bytes_done||0)} ${esc(tr('de'))} ${human(o.bytes_total||0)})</span><div class="bar"><i style="width:${pct}%"></i></div><button class="small" id="libDlCancel">${esc(tr('Cancelar'))}</button>`;
$('libDlCancel').onclick=()=>api('/api/fm/cancel',{}).catch(()=>{}); return }
clearInterval(libDl.t);
box.innerHTML=o.error?`<span>✖ ${esc(tr(o.error))}</span><div class="spacer"></div><button class="small" id="libDlOk">${esc(tr('Cerrar'))}</button>`
:`<span>✔ ${esc(tr('Bajado al PC:'))} <b data-noi18n>${esc(libDl.name)}</b>. ${esc(tr('Lo tienes en «En tu PC».'))}</span><div class="spacer"></div><button class="small" id="libDlFolder">${esc(tr('Abrir carpeta'))}</button><button class="small" id="libDlOk">${esc(tr('Cerrar'))}</button>`;
if($('libDlFolder')) $('libDlFolder').onclick=()=>api('/api/col/openlibdir',{}).catch(()=>{});
$('libDlOk').onclick=()=>{box.hidden=true};
}
['libSearch','libFilter','libSort'].forEach(id=>$(id).addEventListener(id==='libSearch'?'input':'change',libRender));
$('libScan').onclick=()=>libScan(false); $('libRefresh').onclick=()=>libScan(true);
$('libCancel').onclick=()=>api('/api/library/cancel',{});
@@ -2831,7 +2921,7 @@ function suShow(i){
}
if(k==='con'){ $('suTitle').textContent=tr('Comprobar la conexión');
b.innerHTML=`<p>${esc(tr('Miro si la PS5 tiene cargado lo que hace falta. Si algo sale en rojo, pulsa Cargar y lo mando yo.'))}</p><div id="suCon"><p class="hint">${esc(tr('Comprobando…'))}</p></div>
<p class="hint">${esc(tr('Cargar funciona con etaHEN o con un cargador de payloads que acepte la red (puerto 9021). Si usas Y2JB Autoloader, cárgalos desde Payload Manager en la PS5 y pulsa Volver a comprobar.'))}</p>
<p class="hint">${esc(tr('«Cargar» lo hace a través de Payload Manager: ábrelo en la consola (es la aplicación «Payload Manager» del menú de inicio) y pulsa Volver a comprobar. Si no está instalado ftpsrv o BFpilot, se instala solo. También vale etaHEN u otro cargador que acepte la red (puerto 9021).'))}</p>
<div class="actions"><button class="small" id="suRe">Volver a comprobar</button></div>`;
$('suRe').onclick=suCheck; suCheck();
}