Compare commits

...
9 Commits
17 changed files with 2177 additions and 244 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: true
+18
View File
@@ -0,0 +1,18 @@
---
name: Mac (versión en pruebas / testing)
about: Cuéntanos cómo va PS5 Directo en tu Mac / Tell us how PS5 Directo works on your Mac
title: "[Mac] "
labels: mac
---
**Mac y macOS / Mac and macOS version:** (por ejemplo / e.g. MacBook Air M2, macOS 15.1)
**¿Abre? / Does it open?** (sí / no / con avisos — yes / no / with warnings)
**¿Qué funciona y qué falla? / What works and what fails?**
**Informe de fallo / Problem report:** en el programa pulsa «Crear informe de fallo» y arrastra aquí el .zip / in the program click "Create problem report" and drag the .zip here.
Si no llega a abrirse, adjunta si puedes / If it doesn't open, please attach if you can:
`~/Library/Application Support/PS5 Directo/lanzador.log` y / and `~/.ps5directo/ps5directo.log`
+176 -12
View File
@@ -1,6 +1,10 @@
# Publica una version: al subir una etiqueta vX.Y.Z crea la release de GitHub con dos .zip: # Publica una version: al subir una etiqueta vX.Y.Z (o con "Run workflow" en Actions) crea o rehace
# - PS5Directo-X.Y.Z.zip la app (la que usa "Actualizar" dentro del programa) # la release de GitHub con:
# - PS5Directo-X.Y.Z-portable-windows.zip la app + Python incluido: descomprimir y abrir, sin instalar nada # - PS5Directo-X.Y.Z-Setup.exe el instalador: doble clic, «Instalar» y listo. Es la descarga normal.
# - PS5Directo-X.Y.Z-Windows.zip lo mismo sin instalar (con Python incluido). Tambien lo usa
# «Actualizar» dentro del programa, que solo copia los archivos.
# - PS5Directo-X.Y.Z-Linux.tar.gz Linux x86-64, con Python y 7-Zip incluidos (se abre con ps5directo.sh).
# - PS5Directo-X.Y.Z-macOS.dmg Mac (Apple Silicon e Intel), en pruebas: la app con Python y 7-Zip.
name: Publicar version name: Publicar version
on: on:
@@ -14,25 +18,26 @@ permissions:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
ver: ${{ steps.v.outputs.ver }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Version de app.py - name: Version de app.py
id: v id: v
run: echo "ver=$(grep -oP 'VERSION = "\K[^"]+' app.py)" >> "$GITHUB_OUTPUT" run: echo "ver=$(grep -m1 -oP '^VERSION = "\K[^"]+' app.py)" >> "$GITHUB_OUTPUT"
- name: La etiqueta tiene que coincidir con la version - name: La etiqueta tiene que coincidir con la version
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
run: test "${GITHUB_REF_NAME#v}" = "${{ steps.v.outputs.ver }}" run: test "${GITHUB_REF_NAME#v}" = "${{ steps.v.outputs.ver }}"
- name: Paquete normal - name: Archivos del programa
run: | run: |
mkdir -p dist/PS5Directo mkdir -p dist/PS5Directo
git archive HEAD | tar -x -C dist/PS5Directo git archive HEAD | tar -x -C dist/PS5Directo
rm -rf dist/PS5Directo/.github rm -rf dist/PS5Directo/.github dist/PS5Directo/installer
(cd dist && zip -qr "PS5Directo-${{ steps.v.outputs.ver }}.zip" PS5Directo)
- name: Paquete portable (con Python para Windows) - name: Python incluido
run: | run: |
PY=3.12.10 PY=3.12.10
curl -fsSL -o py.zip "https://www.python.org/ftp/python/$PY/python-$PY-embed-amd64.zip" curl -fsSL -o py.zip "https://www.python.org/ftp/python/$PY/python-$PY-embed-amd64.zip"
@@ -40,9 +45,168 @@ jobs:
unzip -q py.zip -d dist/PS5Directo/python unzip -q py.zip -d dist/PS5Directo/python
# que el Python incluido vea la carpeta del programa # que el Python incluido vea la carpeta del programa
for f in dist/PS5Directo/python/python*._pth; do printf '..\n' >> "$f"; done for f in dist/PS5Directo/python/python*._pth; do printf '..\n' >> "$f"; done
(cd dist && zip -qr "PS5Directo-${{ steps.v.outputs.ver }}-portable-windows.zip" PS5Directo)
- uses: softprops/action-gh-release@v2 - name: Zip para Windows
if: startsWith(github.ref, 'refs/tags/') run: (cd dist && zip -qr "PS5Directo-${{ steps.v.outputs.ver }}-Windows.zip" PS5Directo)
# Linux (x86-64): la app con Python y 7-Zip incluidos. Se abre con ps5directo.sh y la primera
# vez se añade sola al menu de aplicaciones.
- name: Paquete para Linux
env:
GH_TOKEN: ${{ github.token }}
run: |
set -e
L=build/linux/PS5Directo
mkdir -p "$L"
git archive HEAD | tar -x -C "$L"
rm -rf "$L/.github" "$L/installer" "$L"/*.bat "$L"/*.vbs
chmod +x "$L/ps5directo.sh"
PBS=20250604; PYV=3.12.11
curl -fsSL -o pyl.tgz "https://github.com/astral-sh/python-build-standalone/releases/download/$PBS/cpython-$PYV+$PBS-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
tar -xzf pyl.tgz -C "$L"
P="$L/python/lib/python3.12"
rm -rf "$P/test" "$P/idlelib" "$P/ensurepip" "$P/lib2to3" "$P/pydoc_data" "$P/turtledemo" \
"$P"/site-packages/pip* "$P/tkinter/test" "$L/python/share" "$L/python/include"
find "$L/python" -name __pycache__ -prune -exec rm -rf {} +
"$L/python/bin/python3" -c "import ssl, sqlite3, ftplib, tkinter, lzma, bz2, ctypes; print('python ok')"
url=$(gh api repos/ip7z/7zip/releases/latest --jq '.assets[] | select(.name | test("linux-x64\\.tar\\.xz$")) | .browser_download_url' | head -1)
mkdir -p "$L/bin" sz
curl -fsSL -o sz.tar.xz "$url"
tar -xJf sz.tar.xz -C sz
cp sz/7zz "$L/bin/7zz"
chmod +x "$L/bin/7zz"
(cd build/linux && tar -czf "../../dist/PS5Directo-${{ steps.v.outputs.ver }}-Linux.tar.gz" PS5Directo)
ls -la dist
- uses: actions/upload-artifact@v4
with: with:
files: dist/*.zip name: dist
path: dist/
retention-days: 3
setup:
needs: build
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Instalador (Inno Setup)
shell: pwsh
run: |
$iscc = Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 6\ISCC.exe'
if (-not (Test-Path $iscc)) { choco install innosetup -y --no-progress | Out-Null }
& $iscc /Q "/DMyVer=${{ needs.build.outputs.ver }}" installer\PS5Directo.iss
if ($LASTEXITCODE -ne 0) { exit 1 }
Get-ChildItem dist\*.exe, dist\*.zip | Format-Table Name, Length
- uses: actions/upload-artifact@v4
with:
name: setup
path: dist/PS5Directo-*-Setup.exe
retention-days: 3
# Mac: "PS5 Directo.app" con Python para Apple Silicon e Intel y 7-Zip, dentro de un .dmg.
# El lanzador (installer/macos/launcher.sh) copia el programa a ~/Library/Application Support
# para que «Actualizar» funcione. Firmada solo "ad hoc" (sin cuenta de desarrollador de Apple).
mac:
needs: build
runs-on: macos-latest
env:
VER: ${{ needs.build.outputs.ver }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- name: Montar la app
run: |
set -e
A="build/PS5 Directo.app"; C="$A/Contents"; R="$C/Resources"
mkdir -p "$C/MacOS" "$R/app" "$R/bin"
git archive HEAD | tar -x -C "$R/app"
rm -rf "$R/app/.github" "$R/app/installer" "$R/app"/*.bat "$R/app"/*.vbs "$R/app/ps5directo.sh"
cp installer/macos/launcher.sh "$C/MacOS/PS5 Directo"
chmod +x "$C/MacOS/PS5 Directo"
sed "s/__VERSION__/$VER/g" installer/macos/Info.plist > "$C/Info.plist"
plutil -lint "$C/Info.plist"
# icono
mkdir -p ps5.iconset
for s in 16 32 128 256 512; do
sips -z $s $s web/icon-512.png --out "ps5.iconset/icon_${s}x${s}.png" >/dev/null
d=$((s*2)); sips -z $d $d web/icon-512.png --out "ps5.iconset/icon_${s}x${s}@2x.png" >/dev/null
done
iconutil -c icns ps5.iconset -o "$R/ps5directo.icns"
# Python para cada procesador
PBS=20250604; PYV=3.12.11
for pair in aarch64:arm64 x86_64:x86_64; do
src=${pair%%:*}; dst=${pair##*:}
curl -fsSL -o py.tgz "https://github.com/astral-sh/python-build-standalone/releases/download/$PBS/cpython-$PYV+$PBS-$src-apple-darwin-install_only_stripped.tar.gz"
rm -rf python && tar -xzf py.tgz
P=python/lib/python3.12
rm -rf "$P/test" "$P/idlelib" "$P/ensurepip" "$P/lib2to3" "$P/pydoc_data" "$P/turtledemo" \
"$P"/site-packages/pip* "$P/tkinter/test" python/share python/include
find python -name __pycache__ -prune -exec rm -rf {} +
mv python "$R/python-$dst"
done
"$R/python-$(uname -m)/bin/python3" -c "import ssl, sqlite3, ftplib, lzma, bz2, ctypes; print('python ok')"
# 7-Zip
url=$(gh api repos/ip7z/7zip/releases/latest --jq '.assets[] | select(.name | test("mac\\.tar\\.xz$")) | .browser_download_url' | head -1)
mkdir -p sz && curl -fsSL -o sz.tar.xz "$url" && tar -xJf sz.tar.xz -C sz
cp sz/7zz "$R/bin/7zz" && chmod +x "$R/bin/7zz"
"$R/bin/7zz" | head -2
codesign --force --deep --sign - "$A"
codesign --verify --deep --verbose=1 "$A"
- name: Prueba de arranque en un Mac
run: |
export HOME="$RUNNER_TEMP/casa"; mkdir -p "$HOME"
"build/PS5 Directo.app/Contents/MacOS/PS5 Directo" --no-browser &
ok=""
for i in $(seq 1 30); do
sleep 1
if grep -q "PS5 Directo $VER" "$HOME/.ps5directo/ps5directo.log" 2>/dev/null; then ok=1; break; fi
done
cat "$HOME/.ps5directo/ps5directo.log" || true
cat "$HOME/Library/Application Support/PS5 Directo/lanzador.log" || true
pkill -f "PS5 Directo/app/app.py" || true
test -n "$ok"
- name: Crear el .dmg
run: |
set -e
mkdir -p dmg dist
cp -R "build/PS5 Directo.app" dmg/
ln -s /Applications dmg/Applications
cp installer/macos/LEEME-Mac.txt "dmg/Si no abre - LEEME.txt"
hdiutil create -volname "PS5 Directo" -srcfolder dmg -ov -format UDZO "dist/PS5Directo-$VER-macOS.dmg"
ls -la dist
- uses: actions/upload-artifact@v4
with:
name: mac
path: dist/PS5Directo-*-macOS.dmg
retention-days: 3
release:
needs: [build, setup, mac]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
# Con una etiqueta crea la release. Pulsando "Run workflow" en Actions añade (o rehace) los archivos
# de la release de la version que pone app.py, aunque ya exista.
- uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.build.outputs.ver }}
files: |
dist/PS5Directo-*-Setup.exe
dist/PS5Directo-*-Windows.zip
dist/PS5Directo-*-Linux.tar.gz
dist/PS5Directo-*-macOS.dmg
+123 -11
View File
@@ -10,8 +10,7 @@ Lo que necesitas antes:
Paso 1. Descarga Paso 1. Descarga
En la página de GitHub del programa, entra en "Releases" y descarga En la página de GitHub del programa, entra en "Releases" y descarga
PS5Directo-X.Y.Z-portable-windows.zip (trae todo, no hay que instalar PS5Directo-X.Y.Z-Windows.zip (trae todo, no hay que instalar nada más). Descomprímelo en una carpeta, por ejemplo en Documentos.
nada más). Descomprímelo en una carpeta, por ejemplo en Documentos.
Paso 2. Ábrelo Paso 2. Ábrelo
Doble clic en "PS5 Directo.vbs". Se abre en su propia ventana y crea un Doble clic en "PS5 Directo.vbs". Se abre en su propia ventana y crea un
@@ -120,8 +119,7 @@ lo tiene y que sabes cargar payloads (.elf).
EN EL PC EN EL PC
- Windows 10 u 11. - Windows 10 u 11.
- Python 3.9 o superior: https://www.python.org/downloads/ - Nada más: el instalador lo trae todo (también Python).
Al instalarlo marca "Add python.exe to PATH".
- 7-Zip (solo si usas "Descomprimir en el PC"): https://www.7-zip.org/ - 7-Zip (solo si usas "Descomprimir en el PC"): https://www.7-zip.org/
EN LA PS5 (se cargan como payloads, por ejemplo con Payload Manager: EN LA PS5 (se cargan como payloads, por ejemplo con Payload Manager:
@@ -155,9 +153,15 @@ BFpilot, sin PS5 Directo.
PRIMERA VEZ PRIMERA VEZ
----------- -----------
Doble clic en "Crear accesos directos.bat". Crea: Abre PS5Directo-X.Y.Z-Setup.exe y pulsa "Instalar". No pide permisos de
- Un icono "PS5 Directo" en el escritorio. administrador. Crea:
- Un icono "PS5 Directo" en el escritorio y en el menú Inicio.
- "Enviar a > PS5 Directo" en el clic derecho de los archivos. - "Enviar a > PS5 Directo" en el clic derecho de los archivos.
Si Windows dice "Windows protegió su PC": "Más información" > "Ejecutar
de todos modos" (el instalador no está firmado; es el aviso normal en
programas pequeños y gratuitos).
Con el .zip en vez del instalador: descomprímelo donde quieras y haz
doble clic en "Crear accesos directos.bat".
Desde entonces abre PS5 Directo con el icono del escritorio: sale en su Desde entonces abre PS5 Directo con el icono del escritorio: sale en su
propia ventana, sin pestañas y sin ventana negra. propia ventana, sin pestañas y sin ventana negra.
@@ -293,14 +297,122 @@ PAYLOADS, DIAGNÓSTICO, MÓVIL Y PROGRAMAR
suspende solo. suspende solo.
VERSIÓN PORTABLE QUÉ DESCARGAR
---------------- -------------
En cada versión hay dos descargas: PS5Directo-X.Y.Z.zip (necesita Python - PS5Directo-X.Y.Z-Setup.exe: el instalador. Es el que debes bajar. Lo
instalado) y PS5Directo-X.Y.Z-portable-windows.zip, que trae Python dentro: trae todo (también Python): ábrelo, pulsa "Instalar" y listo.
descomprimir y abrir, sin instalar nada. - PS5Directo-X.Y.Z-Windows.zip: lo mismo sin instalar, para quien
prefiera descomprimirlo en una carpeta. También lo usa "Actualizar".
Para quitarlo: Configuración de Windows > Aplicaciones > PS5 Directo >
Desinstalar. Tus ajustes se conservan por si vuelves a instalarlo.
- PS5Directo-X.Y.Z-macOS.dmg: para Mac (Apple Silicon e Intel, macOS 11
o posterior). EN PRUEBAS: todavía no se ha probado en un Mac de verdad.
Ábrelo y arrastra PS5 Directo a Aplicaciones. La primera vez macOS la
bloquea porque no está firmada por Apple: en macOS 15 o posterior,
Ajustes del Sistema > Privacidad y seguridad > "Abrir igualmente"; en
anteriores, clic derecho en la app > Abrir. Si algo falla, "Crear
informe de fallo" y adjúntalo en github.com/jstrdev/ps5directo/issues.
- PS5Directo-X.Y.Z-Linux.tar.gz: para Linux (x86-64, distribuciones
recientes como Ubuntu 22.04 o posterior). Descomprímelo donde quieras
y ábrelo una vez con ./ps5directo.sh en una terminal (o clic derecho
> Ejecutar como programa). Esa vez se añade al menú de aplicaciones y
desde entonces lo abres desde ahí. Trae Python y 7-Zip.
Para la ventana sin pestañas usa Chrome, Chromium, Edge o Brave si los
tienes (si no, se abre en tu navegador). Para los torrents hace falta
aria2: sudo apt install aria2. En Linux no hay "Enviar a" ni el aviso
del cortafuegos de Windows.
NOVEDADES 1.22.1
- Mac: la app pide el permiso de «Red local» de macOS. Sin él no puede
hablar con la PS5 aunque esté encendida; ahora, si macOS la bloquea,
lo dice y explica dónde activarlo (Ajustes del Sistema > Privacidad y
seguridad > Red local).
NOVEDADES 1.22.0
- Capturas: botón «Convertir a MP4». Marca los vídeos y los deja en MP4
(H.264) en Descargas > PS5 Directo > Capturas, listos para Telegram,
WhatsApp o cualquier web. Si se grabaron en HDR, los pasa a colores
normales para que no se vean lavados. Usa FFmpeg: en Windows se instala
solo la primera vez (33 MB, pide permiso antes); en Linux y Mac usa el
del sistema (sudo apt install ffmpeg / brew install ffmpeg).
- Espacio: nueva sección «Limpiar restos». Busca envíos y bajadas que se
quedaron a medias, descompresiones cortadas y carpetas temporales, en la
consola y en el PC, con lo que ocupa cada cosa. Lo que todavía usa una
transferencia de la cola sale bloqueado y las carpetas dudosas salen
sin marcar. No se borra nada sin confirmar.
NOVEDADES 1.21.2
- «Bajar al PC» en la Biblioteca: la barra de arriba ahora dice a qué
velocidad baja y cuánto queda.
- La carátula del título que se está bajando se ve apagada y se va
llenando de color de abajo arriba, del 0 al 100 %, con el porcentaje.
- Cada carátula de la Biblioteca lleva un chip pequeño con lo que ocupa.
NOVEDADES 1.21.1
- Biblioteca: botón «Bajar al PC» en cada título (carpeta o imagen), con
su progreso ahí mismo. Llega a la carpeta de PS5 Directo en el PC
(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
un Mac de verdad: se agradece cualquier aviso en GitHub.
- El informe de fallo incluye ahora el registro técnico del programa
(sin el código de acceso de la sesión).
- Arreglado: si cerrabas el programa en la Biblioteca, al abrirlo salía
marcada «Transferencias» con la Biblioteca vacía debajo.
- Versión para Linux (PS5Directo-1.21.0-Linux.tar.gz), con Python y 7-Zip
incluidos. Se añade sola al menú de aplicaciones y se actualiza igual
que en Windows.
- Las descargas al PC van a tu carpeta de Descargas real (en Linux puede
llamarse "Descargas").
NOVEDADES 1.20.4
- Icono propio en la barra de tareas: la ventana ya no se mete dentro del
icono de Edge o Chrome, y al anclarla se ancla PS5 Directo.
- Si ya está abierto y lo vuelves a abrir, trae al frente la ventana que
tenías en vez de abrir otra.
- Si lo anclaste antes de esta versión, desáncla el icono viejo y ancla
el nuevo (clic derecho en el icono de la barra > Anclar).
- Capturas: salen también las hechas con la pantalla en HDR (.jxr), que se
ven convertidas a JPG (al bajarlas al PC se guarda el original). Cada una
dice de qué aplicación es y la fecha, los vídeos llevan la portada de la
aplicación y ya no salen las copias temporales que hace la consola al
compartir.
- Arreglado: en «Archivos de la consola», calcular el tamaño de una
carpeta, borrarla, copiarla, arreglar sus permisos y «Reemplazar» una
carpeta que ya existía fallaban desde la 1.20.3.
NOVEDADES 1.20.3 NOVEDADES 1.20.3
- Instalador para Windows (PS5Directo-1.20.3-Setup.exe): doble clic,
"Instalar" y listo, con todo incluido. Sigue habiendo .zip para quien
no quiera instalar.
- Biblioteca: «Comprobar» en cada aplicación, para cuando no abre o se - Biblioteca: «Comprobar» en cada aplicación, para cuando no abre o se
cierra al arrancar. Revisa la carpeta (ejecutable, param.json, permisos, cierra al arrancar. Revisa la carpeta (ejecutable, param.json, permisos,
firmware, backports y el empaquetado LZ4), muestra los registros (.log) y firmware, backports y el empaquetado LZ4), muestra los registros (.log) y
+23 -23
View File
@@ -1,23 +1,23 @@
' Abre PS5 Directo sin ventana negra (en su propia ventana de aplicacion). ' Abre PS5 Directo sin ventana negra (en su propia ventana de aplicacion).
' Puedes arrastrar archivos encima de este archivo o de su acceso directo. ' Puedes arrastrar archivos encima de este archivo o de su acceso directo.
Set sh = CreateObject("WScript.Shell") Set sh = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject") Set fso = CreateObject("Scripting.FileSystemObject")
dir = fso.GetParentFolderName(WScript.ScriptFullName) dir = fso.GetParentFolderName(WScript.ScriptFullName)
sh.CurrentDirectory = dir sh.CurrentDirectory = dir
args = "" args = ""
For Each a In WScript.Arguments For Each a In WScript.Arguments
args = args & " """ & a & """" args = args & " """ & a & """"
Next Next
py = "" py = ""
If fso.FileExists(dir & "\python\pythonw.exe") Then If fso.FileExists(dir & "\python\pythonw.exe") Then
py = """" & dir & "\python\pythonw.exe""" py = """" & dir & "\python\pythonw.exe"""
ElseIf sh.Run("cmd /c where pyw >nul 2>nul", 0, True) = 0 Then ElseIf sh.Run("cmd /c where pyw >nul 2>nul", 0, True) = 0 Then
py = "pyw -3" py = "pyw -3"
ElseIf sh.Run("cmd /c where pythonw >nul 2>nul", 0, True) = 0 Then ElseIf sh.Run("cmd /c where pythonw >nul 2>nul", 0, True) = 0 Then
py = "pythonw" py = "pythonw"
End If End If
If py = "" Then If py = "" Then
MsgBox "No encuentro Python. Descarga la version portable (trae Python) o instala Python 3 (marca 'Add to PATH').", 48, "PS5 Directo" MsgBox "Faltan archivos de PS5 Directo (la carpeta python). Vuelve a instalarlo con PS5Directo-Setup.exe desde github.com/jstrdev/ps5directo/releases", 48, "PS5 Directo"
Else Else
sh.Run py & " """ & dir & "\app.py""" & args, 0, False sh.Run py & " """ & dir & "\app.py""" & args, 0, False
End If End If
+51 -39
View File
@@ -8,39 +8,33 @@ An all-in-one Windows tool for jailbroken PS5 consoles: it **downloads, converts
<sub>Demo with a sample homebrew app served from a NAS on the local network.</sub> <sub>Demo with a sample homebrew app served from a NAS on the local network.</sub>
- ✅ Streams downloads straight to the console: nothing is saved on the PC ### ⚡ One step instead of three tools
- ✅ Works from download links, ftp:// links (e.g. a NAS), torrents (magnet or .torrent) or files on your PC - ✅ **Download and send at once:** files go straight to the console while they download, nothing stored on your PC
- ✅ Joins split parts (.part1, .part2… / .001, .002…) and extracts RAR, 7z, ZIP and tar (.gz/.bz2/.xz) - ✅ **Or convert on the fly** with [PS5 Dump Forge](https://github.com/quer3q/ps5-dump-forge) (downloaded and updated automatically): folder, `.exfat`, LZ4, `.ffpfsc`, `.ffpkg`, `.ffpfs` or `.fpkg`, with a "Which one?" guide by need
- ✅ Extracts on the console (BFpilot) or on the PC (7-Zip, every file CRC-checked) - ✅ **Any source:** download links, `ftp://` (e.g. a NAS), torrents (magnet or `.torrent`) or files and folders on your PC (drag and drop or "Send to")
- ✅ Fixes the file permissions that stop BFpilot-extracted content from launching (CE-107750-0) - ✅ **Extracts and places:** joins split parts, extracts RAR, 7z, ZIP and tar on the console (BFpilot) or on the PC (7-Zip, CRC-checked), splits archives with a backport and places apps in `/data/homebrew` for ShadowMountPlus
- ✅ Pause, resume and retry where it left off, even after a dropped connection - ✅ **Reliable:** pause, resume and retry where it left off, several connections per file, `.sfv`/`.md5` checks, free-space watch and permission fixes (CE-107750-0)
- ✅ Built-in console file browser (works over plain FTP, BFpilot optional): right-click menu, shortcuts, move/copy to…, extract archives on the console, drag & drop uploads, download to PC
- ✅ Notification bell: finished or failed transfers, disconnections and new versions ### 📚 Your console from your PC and phone
- ✅ Storage view: a pie chart of what fills the console, by app name and size, flagging duplicates and leftovers you can delete - ✅ **Library:** covers, versions and required firmware for everything on the console, including images and installed PS4/PS5 content; flags what won't launch on your firmware, duplicates and broken folders, and links to each title's public update list
- ✅ Send whole PC folders over several connections, skipping what's already there and resuming what got cut off - ✅ **On your PC:** point it at your PC/NAS folders, see what's missing on the console and send it with one click
- ✅ Watches free space so the console never fills up - ✅ **Backup:** saved data, ShadowMountPlus/payload settings and the app database copied to the PC, manually or daily; restore saved data to the same console
- ✅ Speed limit and built-in speed test - ✅ **Console files, Space and Captures:** file browser (copy, move, rename, upload, download), storage chart with "Make room", and the console's screenshots and videos
- ✅ Download-manager style downloads: several reused connections per file, auto-backoff when a host complains - ✅ **Phone and browser:** use it from your phone with a QR code, and send links from your browser with the optional extension
- ✅ Optional browser extension: links arrive by themselves with your browser session (same speed and access as the browser)
- ✅ Drag and drop, or "Send to" from Explorer ### 🩺 When something goes wrong
- ✅ Welcome guide, one-click updates, English and Spanish - ✅ **Diagnosis** on failed transfers: cause, error codes and how to fix it
- ✅ **Library tab**: covers, versions and the firmware each app needs, flagging what won't launch on your console (backport-aware), duplicates and incomplete folders. Also shows what's installed (PS4 and PS5), your PC/NAS collection with what's missing on the console, and links to each title's public update list - ✅ **Launch check:** if an app closes on launch, it checks the folder (executable, permissions, firmware, backports, LZ4 packing) and shows recent logs and captures
- ✅ **Backup tab**: saved data, ShadowMountPlus/payload settings and the app database copied to the PC, manually or daily; restore saved data to the same console - ✅ **Problem report** in one click (.zip, no passwords) to share when asking for help
- ✅ **Captures tab**: view the console's screenshots and videos and download them - ✅ **Warnings:** your PS5 firmware always visible, speed-drop alerts, "Why is it slow?" network check, and it loads ftpsrv/BFpilot from the app
- ✅ **Diagnosis** on failed transfers (cause, codes and fixes) and a one-click problem report (.zip, no passwords) - ✅ **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
- ✅ **Format converter** with [PS5 Dump Forge](https://github.com/quer3q/ps5-dump-forge) (optional): send it as is, or turn it on the PC into a plain folder, `.exfat`, LZ4, `.ffpfsc`, `.ffpkg`, `.ffpfs` or `.fpkg`, with a "Which one?" guide by need. Archives with a normal version plus a backport are split automatically - ✅ **Self-updating**, with a welcome window and a user guide, in English and Spanish
- ✅ Shows your PS5 firmware at the top and tells you which formats and apps will work on it
- ✅ "Download again" from the queue or the history, without pasting the link
- ✅ Asks before sending something that's already on the console (send anyway, replace or cancel)
- ✅ Checks every part against its `.sfv` / `.md5` / `.sha1` while sending
- ✅ "Make room": suggests what to delete when space runs out
- ✅ Loads ftpsrv and BFpilot on the PS5 from the app (ELF loader, port 9021)
- ✅ "Why is it slow?" network diagnostics (Wi-Fi band and signal, 100 Mb/s cables, VPNs, ping)
- ✅ Open it from your phone with a QR code; schedule the queue and sleep or shut down the PC when it's done
## Requirements ## Requirements
- **PC:** Windows 10/11. Get the `-portable-windows.zip` (Python included, nothing to install), or the plain `.zip` with [Python 3.9+](https://www.python.org/downloads/) installed (tick "Add python.exe to PATH"). For extracting on the PC, also install [7-Zip](https://www.7-zip.org/). - **PC:** Windows 10/11. Nothing else to install: the download includes everything it needs. Optional: [7-Zip](https://www.7-zip.org/), only if you want to extract on the PC instead of on the console.
- **Mac (new, in testing — not tested on a real Mac yet, [feedback welcome](../../issues/new?template=mac.md)):** macOS 11 or later, Apple Silicon or Intel. Python and 7-Zip are included. Torrents need `aria2` (`brew install aria2`).
- **Linux (new, x86-64):** any recent distro (glibc 2.35+, e.g. Ubuntu 22.04+). Python and 7-Zip are included. For the app window without tabs, Chrome, Chromium, Edge or Brave (otherwise it opens in your default browser). Torrents need `aria2` (`sudo apt install aria2`).
- **Jailbroken PS5:** - **Jailbroken PS5:**
- An FTP server, for example [ftpsrv](https://github.com/ps5-payload-dev/ftpsrv) on port 2121. - An FTP server, for example [ftpsrv](https://github.com/ps5-payload-dev/ftpsrv) on port 2121.
- [BFpilot](https://github.com/ItsBlurf/BFpilot) on port 5905. - [BFpilot](https://github.com/ItsBlurf/BFpilot) on port 5905.
@@ -49,10 +43,17 @@ An all-in-one Windows tool for jailbroken PS5 consoles: it **downloads, converts
## Getting started ## Getting started
1. Download the `-portable-windows.zip` (or the plain `.zip` if you have Python) from the latest [release](../../releases) and extract it anywhere. 1. Download **`PS5Directo-X.Y.Z-Setup.exe`** from the latest [release](../../releases), open it and click **Install**. No admin rights needed, nothing else to install.
2. Run `Crear accesos directos.bat` ("create shortcuts"). It adds a desktop icon and a "Send to → PS5 Directo" entry. 2. Open **PS5 Directo** from the desktop or the Start menu. The language follows Windows; switch it with the **English / Español** button at the top.
3. Open **PS5 Directo** from the desktop. The language follows Windows; switch it with the **English / Español** button at the top. 3. Follow the welcome guide, enter your PS5's IP in Settings, then paste links or drop files.
4. Follow the welcome guide, enter your PS5's IP in Settings, then paste links or drop files.
> If Windows shows "Windows protected your PC", click **More info → Run anyway**. The installer isn't signed (signing costs money); it's the usual warning for small free programs.
>
> Rather not install? Download `PS5Directo-X.Y.Z-Windows.zip` instead, extract it anywhere and run `Crear accesos directos.bat`. Both update themselves the same way.
**Mac (in testing):** download `PS5Directo-X.Y.Z-macOS.dmg`, open it and drag **PS5 Directo** into Applications. The app isn't signed by Apple, so the first time macOS blocks it: on macOS 15+ go to *System Settings → Privacy & Security → Open Anyway*; on older versions right-click the app → *Open*. **It hasn't been tested on a real Mac yet:** if something fails, click *Create problem report* in the program and attach it to an [issue](../../issues/new?template=mac.md). Thanks!
**Linux:** download `PS5Directo-X.Y.Z-Linux.tar.gz`, extract it anywhere (for example in your home folder) and open it once with `./ps5directo.sh` in a terminal (or right-click → *Run as a Program*). That first time it adds **PS5 Directo** to your applications menu, so from then on you open it from there. It updates itself like on Windows.
> **About BFpilot and permissions:** up to at least v0.4.4, BFpilot's archive extractor leaves files without the execute bit, so the PS5 refuses to launch them (CE-107750-0). This also happens when you extract from BFpilot's own web UI. PS5 Directo fixes it automatically after every extraction. For content you already have, use **Settings → Fix permissions**. > **About BFpilot and permissions:** up to at least v0.4.4, BFpilot's archive extractor leaves files without the execute bit, so the PS5 refuses to launch them (CE-107750-0). This also happens when you extract from BFpilot's own web UI. PS5 Directo fixes it automatically after every extraction. For content you already have, use **Settings → Fix permissions**.
@@ -60,7 +61,7 @@ The full guide is in [README_EN.txt](README_EN.txt) (Spanish: [LEEME.txt](LEEME.
## Publishing a release ## Publishing a release
Bump `VERSION` in `app.py`, add the notes to `LEEME.txt` / `README_EN.txt`, then push a tag (`git tag v1.20.3 && git push --tags`). The GitHub Action builds both zips (plain and portable with Python) and creates the release. Bump `VERSION` in `app.py`, add the notes to `LEEME.txt` / `README_EN.txt`, then push a tag (`git tag v1.20.3 && git push --tags`). The GitHub Action builds the installer `PS5Directo-X.Y.Z-Setup.exe`, `PS5Directo-X.Y.Z-Windows.zip`, `PS5Directo-X.Y.Z-Linux.tar.gz` and `PS5Directo-X.Y.Z-macOS.dmg` (all with Python included; the Mac app is built and smoke-tested on a GitHub macOS runner) and creates the release. The installer script is `installer/PS5Directo.iss` (Inno Setup).
## Disclaimer ## Disclaimer
@@ -79,12 +80,23 @@ Independent project, not affiliated with Sony or with the authors of the tools m
Herramienta todo en uno para Windows y PS5 con jailbreak: **descarga, convierte y envía** en un solo paso. Tal cual, los archivos van a la consola por FTP **mientras se descargan**, sin pasar por el disco del PC; o los **convierte sobre la marcha** (LZ4, `.exfat`, carpeta, `.ffpfsc`…) dentro del mismo trabajo, sin usar otro programa. Descomprime en la consola (BFpilot) o en el PC (7-Zip), coloca las aplicaciones en `/data/homebrew` para que ShadowMountPlus las muestre en el menú, y te deja ver la biblioteca de la consola con portadas y controlarlo todo desde el móvil. Herramienta todo en uno para Windows y PS5 con jailbreak: **descarga, convierte y envía** en un solo paso. Tal cual, los archivos van a la consola por FTP **mientras se descargan**, sin pasar por el disco del PC; o los **convierte sobre la marcha** (LZ4, `.exfat`, carpeta, `.ffpfsc`…) dentro del mismo trabajo, sin usar otro programa. Descomprime en la consola (BFpilot) o en el PC (7-Zip), coloca las aplicaciones en `/data/homebrew` para que ShadowMountPlus las muestre en el menú, y te deja ver la biblioteca de la consola con portadas y controlarlo todo desde el móvil.
**Novedades 1.20.3:** ventana de bienvenida, Biblioteca con lo instalado (PS4 y PS5) y con tu colección del PC/NAS, copias de seguridad de partidas y ajustes, capturas y vídeos, diagnóstico de errores con informe, torrents y avisos de actualización automáticos. **Un programa en vez de tres:** descarga y envío a la vez sin ocupar el PC, o conversión sobre la marcha (carpeta, `.exfat`, LZ4, `.ffpfsc`, `.ffpkg`, `.ffpfs`, `.fpkg`) con [PS5 Dump Forge](https://github.com/quer3q/ps5-dump-forge). Vale con enlaces, `ftp://`, torrents o archivos del PC; une partes, descomprime y lo deja colocado.
**Novedades 1.20:** pestaña Biblioteca con portadas y aviso de firmware, convertidor de formatos con [PS5 Dump Forge](https://github.com/quer3q/ps5-dump-forge) (carpeta, `.exfat`, LZ4, `.ffpfsc`, `.ffpkg`, `.ffpfs` y `.fpkg`), firmware de tu PS5 siempre a la vista, «Descargar de nuevo», pregunta si ya está en la consola, comprobación con `.sfv`/`.md5`, "Hacer sitio", cargar ftpsrv/BFpilot desde el programa, diagnóstico de red, acceso desde el móvil con QR y programar la cola. **Tu consola desde el PC y el móvil:** Biblioteca con portadas, versión y firmware (también lo instalado de PS4 y PS5), vista «En tu PC» para enviar lo que falta, copias de seguridad de partidas y ajustes, explorador de archivos, espacio, capturas y modo móvil con QR.
**Requisitos:** Windows 10/11. Descarga el `-portable-windows.zip` (trae Python, no hay que instalar nada) o el `.zip` normal con [Python 3.9+](https://www.python.org/downloads/). Para descomprimir en el PC, [7-Zip](https://www.7-zip.org/). En la PS5: servidor FTP ([ftpsrv](https://github.com/ps5-payload-dev/ftpsrv)), [BFpilot](https://github.com/ItsBlurf/BFpilot) y [ShadowMountPlus](https://github.com/drakmor/ShadowMountPlus). **Cuando algo falla:** diagnóstico de errores con causa y solución, «Comprobar» si algo se cierra al abrirlo, informe de fallo en un clic y avisos automáticos de versiones y de bajadas de velocidad.
**Uso:** descarga el `.zip` de la última [release](../../releases), ejecuta `Crear accesos directos.bat` y abre **PS5 Directo** desde el escritorio. La guía completa está en [LEEME.txt](LEEME.txt). **Requisitos:** Windows 10/11, sin instalar nada más: la descarga lo trae todo. Opcional: [7-Zip](https://www.7-zip.org/), solo si quieres descomprimir en el PC en vez de en la consola. En la PS5: servidor FTP ([ftpsrv](https://github.com/ps5-payload-dev/ftpsrv)), [BFpilot](https://github.com/ItsBlurf/BFpilot) y [ShadowMountPlus](https://github.com/drakmor/ShadowMountPlus).
**Cómo instalarlo:**
1. Descarga **`PS5Directo-X.Y.Z-Setup.exe`** de la última [release](../../releases), ábrelo y pulsa **Instalar**. No pide permisos de administrador ni hay que instalar nada más.
2. Abre **PS5 Directo** desde el escritorio o el menú Inicio.
3. Sigue la ventana de bienvenida y pon la IP de tu PS5 en Ajustes.
Si Windows dice «Windows protegió su PC», pulsa **Más información → Ejecutar de todos modos** (el instalador no está firmado, que cuesta dinero; es el aviso normal en programas pequeños y gratuitos). ¿Prefieres no instalar? Descarga el `.zip`, descomprímelo y ejecuta `Crear accesos directos.bat`. La guía completa está en [LEEME.txt](LEEME.txt).
**Mac (nuevo, en pruebas):** descarga `PS5Directo-X.Y.Z-macOS.dmg`, ábrelo y arrastra **PS5 Directo** a Aplicaciones. No está firmada por Apple, así que la primera vez macOS la bloquea: en macOS 15 o posterior ve a *Ajustes del Sistema → Privacidad y seguridad → Abrir igualmente*; en versiones anteriores, clic derecho en la app → *Abrir*. **Todavía no se ha podido probar en un Mac de verdad:** si algo falla, pulsa *Crear informe de fallo* en el programa y adjúntalo en un [aviso](../../issues/new?template=mac.md). ¡Gracias!
**Linux (nuevo):** descarga `PS5Directo-X.Y.Z-Linux.tar.gz`, descomprímelo donde quieras y ábrelo una vez con `./ps5directo.sh` en una terminal (o clic derecho → *Ejecutar como programa*). Esa primera vez se añade solo al menú de aplicaciones, y desde entonces lo abres desde ahí. Trae Python y 7-Zip; para los torrents hace falta `aria2` (`sudo apt install aria2`).
**Aviso:** proyecto independiente, sin relación con Sony ni con los autores de las herramientas citadas. No incluye contenido de terceros, claves ni archivos de Sony, ni enlaces a sitios de descargas. Úsalo solo con contenido que tengas derecho a usar (copias de seguridad propias y homebrew). No hace el jailbreak ni salta ninguna protección: solo copia archivos a una consola que ya has modificado tú. No se da ayuda para conseguir contenido sin licencia. Se ofrece tal cual, sin garantías, y lo que hagas con él es responsabilidad tuya. **Aviso:** proyecto independiente, sin relación con Sony ni con los autores de las herramientas citadas. No incluye contenido de terceros, claves ni archivos de Sony, ni enlaces a sitios de descargas. Úsalo solo con contenido que tengas derecho a usar (copias de seguridad propias y homebrew). No hace el jailbreak ni salta ninguna protección: solo copia archivos a una consola que ya has modificado tú. No se da ayuda para conseguir contenido sin licencia. Se ofrece tal cual, sin garantías, y lo que hagas con él es responsabilidad tuya.
+117 -9
View File
@@ -6,7 +6,7 @@ QUICK START (5 MINUTES)
You need: a PS5 that is already jailbroken (PS5 Directo doesn't do that) You need: a PS5 that is already jailbroken (PS5 Directo doesn't do that)
and a Windows 10/11 PC on the same network. and a Windows 10/11 PC on the same network.
1. Download PS5Directo-X.Y.Z-portable-windows.zip from the GitHub Releases 1. Download PS5Directo-X.Y.Z-Windows.zip from the GitHub Releases
page (nothing else to install) and extract it, e.g. into Documents. page (nothing else to install) and extract it, e.g. into Documents.
2. Double-click "PS5 Directo.vbs". If Windows asks whether to open a file 2. Double-click "PS5 Directo.vbs". If Windows asks whether to open a file
downloaded from the internet, click "Open". downloaded from the internet, click "Open".
@@ -96,8 +96,7 @@ jailbroken and that you know how to load payloads (.elf).
ON THE PC ON THE PC
- Windows 10 or 11. - Windows 10 or 11.
- Python 3.9 or newer: https://www.python.org/downloads/ - Nothing else: the installer includes everything (Python too).
When installing, tick "Add python.exe to PATH".
- 7-Zip (only for "Extract on the PC"): https://www.7-zip.org/ - 7-Zip (only for "Extract on the PC"): https://www.7-zip.org/
ON THE PS5 (loaded as payloads, for example with Payload Manager: ON THE PS5 (loaded as payloads, for example with Payload Manager:
@@ -130,9 +129,15 @@ BFpilot's own web page, without PS5 Directo.
FIRST TIME FIRST TIME
---------- ----------
Double-click "Crear accesos directos.bat" ("create shortcuts"). It adds: Open PS5Directo-X.Y.Z-Setup.exe and click "Install". No admin rights
- A "PS5 Directo" icon on the desktop. needed. It adds:
- A "PS5 Directo" icon on the desktop and in the Start menu.
- "Send to > PS5 Directo" in the right-click menu of your files. - "Send to > PS5 Directo" in the right-click menu of your files.
If Windows says "Windows protected your PC": "More info" > "Run anyway"
(the installer isn't signed; it's the usual warning for small free
programs).
With the .zip instead of the installer: extract it anywhere and
double-click "Crear accesos directos.bat" ("create shortcuts").
From then on, open PS5 Directo from the desktop icon: it runs in its own From then on, open PS5 Directo from the desktop icon: it runs in its own
window, with no tabs and no black console window. window, with no tabs and no black console window.
@@ -306,14 +311,117 @@ PAYLOADS, DIAGNOSTICS, PHONE AND SCHEDULE
- "Schedule" (queue): start at a set time and sleep or shut down the PC when - "Schedule" (queue): start at a set time and sleep or shut down the PC when
done. The PC doesn't go to sleep while transfers are running. done. The PC doesn't go to sleep while transfers are running.
PORTABLE VERSION WHAT TO DOWNLOAD
---------------- ----------------
Each release has PS5Directo-X.Y.Z.zip (needs Python installed) and - PS5Directo-X.Y.Z-Setup.exe: the installer. This is the one to get.
PS5Directo-X.Y.Z-portable-windows.zip, which includes Python: extract and run. Everything included (Python too): open it, click "Install", done.
- PS5Directo-X.Y.Z-Windows.zip: the same without installing, if you'd
rather extract it to a folder. "Update" uses it too.
To remove it: Windows Settings > Apps > PS5 Directo > Uninstall. Your
settings are kept in case you install it again.
- PS5Directo-X.Y.Z-macOS.dmg: for Mac (Apple Silicon and Intel, macOS 11
or later). IN TESTING: not tested on a real Mac yet. Open it and drag
PS5 Directo into Applications. The first time macOS blocks it because
it isn't signed by Apple: on macOS 15 or later, System Settings >
Privacy & Security > "Open Anyway"; on older ones, right-click the app
> Open. If something fails, "Create problem report" and attach it at
github.com/jstrdev/ps5directo/issues.
- PS5Directo-X.Y.Z-Linux.tar.gz: for Linux (x86-64, recent distros such
as Ubuntu 22.04 or later). Extract it anywhere and open it once with
./ps5directo.sh in a terminal (or right-click > Run as a Program).
That first time it adds itself to the applications menu, so from then
on you open it from there. Python and 7-Zip are included.
For the window without tabs it uses Chrome, Chromium, Edge or Brave if
you have one (otherwise your browser). Torrents need aria2:
sudo apt install aria2. Linux has no "Send to" and no Windows firewall
prompt.
WHAT'S NEW WHAT'S NEW
---------- ----------
1.20.3 - Library: "Check" on each app, for when it won't open or closes on 1.22.1 - Mac: the app asks for macOS "Local Network" permission. Without
it, it can't talk to the PS5 even when it's on; now, if macOS
blocks it, it says so and explains where to turn it on (System
Settings > Privacy & Security > Local Network).
1.22.0 - Captures: "Convert to MP4" button. Tick the videos and they're
saved as MP4 (H.264) in Downloads > PS5 Directo > Capturas, ready
for Telegram, WhatsApp or any website. If they were recorded in
HDR, they get normal colours so they don't look washed out. Uses
FFmpeg: on Windows it's installed the first time only (33 MB, it
asks first); on Linux and Mac it uses the system one.
- Space: new "Clean up leftovers" section. Finds half-finished
sends and downloads, cut-off extractions and temporary folders on
the console and on the PC, with the size of each. Anything a
queued transfer still uses is locked and doubtful folders come
unticked. Nothing is deleted without confirmation.
1.21.2 - Library "Download to PC": the bar at the top now shows the speed
and the time left.
- The cover of the title being downloaded turns dim and fills with
colour from bottom to top, 0 to 100 %, with the percentage.
- Each Library cover has a small chip with its size.
1.21.1 - Library: "Download to PC" button on each title (folder or image),
with its progress right there. It goes to the PS5 Directo folder
on the PC (by default Downloads > PS5 Directo > Biblioteca, you
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.
- The problem report now includes the program's technical log
(without the session access code).
- Fixed: if you closed the program on the Library tab, on reopening
it showed "Transfers" selected with an empty Library below.
- Linux version (PS5Directo-1.21.0-Linux.tar.gz), with Python and
7-Zip included. It adds itself to the applications menu and
updates itself like on Windows.
- Downloads to the PC go to your real Downloads folder (on Linux it
may have another name).
1.20.4 - Its own taskbar icon: the window no longer hides inside the Edge
or Chrome icon, and pinning it pins PS5 Directo.
- If it's already open and you open it again, it brings the existing
window to the front instead of opening another one.
- If you pinned it before this version, unpin the old icon and pin
the new one (right-click the taskbar icon > Pin).
- Captures: screenshots taken with HDR on (.jxr) now show up,
previewed as JPG (downloading keeps the original). Each one shows
the app and the date, videos show the app's cover, and the
temporary copies the console makes when sharing are hidden.
- Fixed: in "Console files", folder size, deleting, copying, fixing
permissions and "Replace" on an existing folder failed since 1.20.3.
1.20.3 - Windows installer (PS5Directo-1.20.3-Setup.exe): double-click,
"Install", done, everything included. The .zip is still there if
you'd rather not install.
- Library: "Check" on each app, for when it won't open or closes on
launch. It checks the folder (executable, param.json, permissions, launch. It checks the folder (executable, param.json, permissions,
firmware, backports and LZ4 packing), shows logs (.log) and captures firmware, backports and LZ4 packing), shows logs (.log) and captures
from the last minutes, explains the error code shown on the TV and from the last minutes, explains the error code shown on the TV and
+926 -63
View File
File diff suppressed because it is too large. Load diff
+185 -13
View File
@@ -772,6 +772,92 @@ def fetch_payload(pid, data_dir, log=None):
return dest, tag 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): def send_elf(host, port, data, timeout=15):
"""Manda un payload al cargador de ELF de la consola (elfldr/etaHEN, puerto 9021).""" """Manda un payload al cargador de ELF de la consola (elfldr/etaHEN, puerto 9021)."""
host = E.clean_host(host)[0] host = E.clean_host(host)[0]
@@ -1211,13 +1297,41 @@ def diagnose(ps5_ip, cfg, speed=None, active=0):
# Energia: no dormir mientras hay trabajo y apagar/suspender al acabar # Energia: no dormir mientras hay trabajo y apagar/suspender al acabar
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_AWAKE = {"proc": None}
def keep_awake(on): def keep_awake(on):
if os.name != "nt": """Que el PC no se suspenda mientras hay envios. En Linux/Mac se hace con un proceso
(systemd-inhibit / caffeinate) que dura mientras haga falta."""
if os.name == "nt":
try:
import ctypes
ES_CONTINUOUS, ES_SYSTEM_REQUIRED = 0x80000000, 0x00000001
ctypes.windll.kernel32.SetThreadExecutionState(ES_CONTINUOUS | (ES_SYSTEM_REQUIRED if on else 0))
return True
except Exception:
return False
p = _AWAKE["proc"]
if not on:
if p is not None:
try:
p.terminate()
except Exception:
pass
_AWAKE["proc"] = None
return True
if p is not None and p.poll() is None:
return True
if sys.platform == "darwin":
cmd = ["caffeinate", "-i", "-w", str(os.getpid())]
elif shutil.which("systemd-inhibit"):
cmd = ["systemd-inhibit", "--what=sleep:idle", "--who=PS5 Directo", "--why=Enviando a la PS5",
"--mode=block", "sleep", "infinity"]
else:
return False return False
try: try:
import ctypes _AWAKE["proc"] = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
ES_CONTINUOUS, ES_SYSTEM_REQUIRED = 0x80000000, 0x00000001 stderr=subprocess.DEVNULL)
ctypes.windll.kernel32.SetThreadExecutionState(ES_CONTINUOUS | (ES_SYSTEM_REQUIRED if on else 0))
return True return True
except Exception: except Exception:
return False return False
@@ -1225,15 +1339,73 @@ def keep_awake(on):
def power_action(kind): def power_action(kind):
"""'sleep' (suspender) o 'shutdown' (apagar). Devuelve un texto de lo hecho.""" """'sleep' (suspender) o 'shutdown' (apagar). Devuelve un texto de lo hecho."""
if os.name != "nt": if os.name == "nt":
return "Solo funciona en Windows" if kind == "shutdown":
if kind == "shutdown": subprocess.Popen(["shutdown", "/s", "/t", "0", "/c", "PS5 Directo: cola terminada"], creationflags=NO_WINDOW)
subprocess.Popen(["shutdown", "/s", "/t", "0", "/c", "PS5 Directo: cola terminada"], creationflags=NO_WINDOW) return "Apagando el PC"
return "Apagando el PC" if kind == "sleep":
if kind == "sleep": subprocess.Popen(["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"], creationflags=NO_WINDOW)
subprocess.Popen(["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"], creationflags=NO_WINDOW) return "Suspendiendo el PC"
return "Suspendiendo el PC" return ""
return "" keep_awake(False)
if sys.platform == "darwin":
cmd = {"shutdown": ["osascript", "-e", 'tell application "System Events" to shut down'],
"sleep": ["pmset", "sleepnow"]}.get(kind)
else:
cmd = {"shutdown": ["systemctl", "poweroff"], "sleep": ["systemctl", "suspend"]}.get(kind)
if not cmd:
return ""
try:
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception as e:
return "No pude: %s" % e
return "Apagando el PC" if kind == "shutdown" else "Suspendiendo el PC"
def open_path(path):
"""Abre una carpeta o archivo del PC con el programa del sistema (Explorador, Finder...)."""
if os.name == "nt":
os.startfile(path)
return
cmd = ["open", path] if sys.platform == "darwin" else ["xdg-open", path]
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def pick_native(kind, title):
"""Selector de archivos/carpeta del sistema en Linux (zenity/kdialog) o Mac (osascript).
Devuelve la lista elegida ([] si se cancela) o None si no hay ninguno disponible."""
title = title or "PS5 Directo"
if sys.platform == "darwin":
if kind == "folder":
script = 'POSIX path of (choose folder with prompt %s)' % _as_str(title)
else:
script = ('set l to choose file with prompt %s with multiple selections allowed\n'
'set o to ""\nrepeat with f in l\nset o to o & POSIX path of f & linefeed\nend repeat\nreturn o'
% _as_str(title))
args = ["osascript"]
for ln in script.split("\n"):
args += ["-e", ln]
p = subprocess.run(args, capture_output=True, timeout=3600)
if p.returncode != 0:
return []
return [x.rstrip("/") or "/" for x in p.stdout.decode("utf-8", "replace").splitlines() if x.strip()]
if shutil.which("zenity"):
cmd = ["zenity", "--file-selection", "--title=" + title, "--separator=\n"]
cmd += ["--directory"] if kind == "folder" else ["--multiple"]
elif shutil.which("kdialog"):
home = os.path.expanduser("~")
cmd = (["kdialog", "--title", title, "--getexistingdirectory", home] if kind == "folder" else
["kdialog", "--title", title, "--getopenfilename", home, "--multiple", "--separate-output"])
else:
return None
p = subprocess.run(cmd, capture_output=True, timeout=3600)
if p.returncode != 0:
return []
return [x for x in p.stdout.decode("utf-8", "replace").splitlines() if x.strip()]
def _as_str(t):
return '"' + str(t).replace("\\", "\\\\").replace('"', '\\"') + '"'
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+74
View File
@@ -0,0 +1,74 @@
; Instalador de PS5 Directo (Inno Setup 6). Lo compila GitHub Actions en cada version:
; ISCC /DMyVer=X.Y.Z installer\PS5Directo.iss
; Espera la carpeta del programa ya montada (con Python incluido) en dist\PS5Directo
; y deja dist\PS5Directo-X.Y.Z-Setup.exe.
; Se instala para el usuario actual (sin pedir permisos de administrador), en
; %LOCALAPPDATA%\Programs\PS5 Directo, para que «Actualizar» pueda escribir en la carpeta.
#ifndef MyVer
#define MyVer "0.0.0"
#endif
#ifndef SrcDir
#define SrcDir "..\dist\PS5Directo"
#endif
[Setup]
AppId={{BD789BF5-0254-4A4C-92CB-332BB22CEE42}
AppName=PS5 Directo
AppVersion={#MyVer}
AppVerName=PS5 Directo {#MyVer}
AppPublisher=PS5 Directo
AppPublisherURL=https://github.com/jstrdev/ps5directo
AppSupportURL=https://github.com/jstrdev/ps5directo/issues
AppUpdatesURL=https://github.com/jstrdev/ps5directo/releases
DefaultDirName={localappdata}\Programs\PS5 Directo
DefaultGroupName=PS5 Directo
DisableProgramGroupPage=yes
DisableReadyPage=no
DisableDirPage=yes
UsePreviousAppDir=yes
PrivilegesRequired=lowest
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
MinVersion=10.0
OutputDir=..\dist
OutputBaseFilename=PS5Directo-{#MyVer}-Setup
SetupIconFile={#SrcDir}\web\ps5directo.ico
WizardSmallImageFile=small.bmp,small2x.bmp
UninstallDisplayIcon={app}\web\ps5directo.ico
UninstallDisplayName=PS5 Directo
Compression=lzma2/max
SolidCompression=yes
WizardStyle=modern
CloseApplications=yes
RestartApplications=no
ShowLanguageDialog=no
LanguageDetectionMethod=uilanguage
[Languages]
Name: "es"; MessagesFile: "compiler:Languages\Spanish.isl"
Name: "en"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "{#SrcDir}\*"; DestDir: "{app}"; Excludes: "Crear accesos directos.bat,__pycache__"; Flags: ignoreversion recursesubdirs createallsubdirs
[InstallDelete]
; restos de una version anterior que ya no se usan
Type: files; Name: "{app}\Crear accesos directos.bat"
[Icons]
Name: "{autoprograms}\PS5 Directo"; Filename: "{sys}\wscript.exe"; Parameters: """{app}\PS5 Directo.vbs"""; WorkingDir: "{app}"; IconFilename: "{app}\web\ps5directo.ico"; Comment: "PS5 Directo"; AppUserModelID: "PS5Directo.App"
Name: "{autodesktop}\PS5 Directo"; Filename: "{sys}\wscript.exe"; Parameters: """{app}\PS5 Directo.vbs"""; WorkingDir: "{app}"; IconFilename: "{app}\web\ps5directo.ico"; Comment: "PS5 Directo"; AppUserModelID: "PS5Directo.App"
Name: "{usersendto}\PS5 Directo"; Filename: "{sys}\wscript.exe"; Parameters: """{app}\PS5 Directo.vbs"""; WorkingDir: "{app}"; IconFilename: "{app}\web\ps5directo.ico"; Comment: "PS5 Directo"; AppUserModelID: "PS5Directo.App"
[Run]
Filename: "{sys}\wscript.exe"; Parameters: """{app}\PS5 Directo.vbs"""; WorkingDir: "{app}"; Description: "{cm:LaunchProgram,PS5 Directo}"; Flags: postinstall nowait skipifsilent
[UninstallRun]
; cierra PS5 Directo si sigue abierto, para poder borrar sus archivos
Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Get-Process python,pythonw -ErrorAction SilentlyContinue | Where-Object {{ $_.Path -like '{app}\*' } | Stop-Process -Force"""; Flags: runhidden; RunOnceId: "CerrarPS5Directo"
[UninstallDelete]
; tambien lo que añadio «Actualizar» despues de instalar. Los ajustes del usuario
; (carpeta .ps5directo) no se tocan.
Type: filesandordirs; Name: "{app}"
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>PS5 Directo</string>
<key>CFBundleDisplayName</key><string>PS5 Directo</string>
<key>CFBundleIdentifier</key><string>io.github.jstrdev.ps5directo</string>
<key>CFBundleExecutable</key><string>PS5 Directo</string>
<key>CFBundleIconFile</key><string>ps5directo</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>__VERSION__</string>
<key>CFBundleVersion</key><string>__VERSION__</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>LSMinimumSystemVersion</key><string>11.0</string>
<key>NSHighResolutionCapable</key><true/>
<key>NSLocalNetworkUsageDescription</key><string>PS5 Directo se conecta a tu PS5 por la red local (FTP, BFpilot y Payload Manager). / PS5 Directo connects to your PS5 over the local network (FTP, BFpilot and Payload Manager).</string>
<key>LSApplicationCategoryType</key><string>public.app-category.utilities</string>
</dict>
</plist>
+64
View File
@@ -0,0 +1,64 @@
PS5 DIRECTO PARA MAC (EN PRUEBAS)
=================================
Esta versión todavía no se ha podido probar en un Mac de verdad. Si algo
falla, en el programa pulsa "Crear informe de fallo" y adjúntalo en un
aviso en GitHub: https://github.com/jstrdev/ps5directo/issues
Con eso se arregla rápido. ¡Gracias por probarla!
INSTALAR
1. Arrastra "PS5 Directo" a la carpeta Aplicaciones (Applications).
2. Ábrelo desde Aplicaciones.
SI MACOS NO TE DEJA ABRIRLO
La app no está firmada por Apple (eso cuesta 99 $ al año), así que la
primera vez macOS la bloquea. Es normal en programas gratuitos.
- macOS 15 (Sequoia) o posterior: intenta abrirla, cierra el aviso y ve a
Ajustes del Sistema > Privacidad y seguridad. Abajo verás "Se ha
bloqueado PS5 Directo": pulsa "Abrir igualmente".
- macOS 14 o anterior: clic derecho (o Control + clic) sobre la app >
Abrir > Abrir.
- Si aun así no abre, en Terminal:
xattr -dr com.apple.quarantine "/Applications/PS5 Directo.app"
NOTAS
- Funciona en Mac con Apple Silicon (M1, M2...) e Intel, macOS 11 o
posterior. Trae Python y 7-Zip.
- Para la ventana sin pestañas usa Chrome, Edge o Brave si los tienes; si
no, se abre en Safari.
- Para los torrents hace falta aria2: instala Homebrew (brew.sh) y luego
"brew install aria2".
- Se actualiza solo desde el propio programa, como en Windows.
PS5 DIRECTO FOR MAC (TESTING)
=============================
This version hasn't been tested on a real Mac yet. If something fails,
click "Create problem report" in the program and attach it to an issue
on GitHub: https://github.com/jstrdev/ps5directo/issues
That makes it quick to fix. Thanks for trying it!
INSTALL
1. Drag "PS5 Directo" into the Applications folder.
2. Open it from Applications.
IF MACOS WON'T OPEN IT
The app isn't signed by Apple (that costs $99 a year), so macOS blocks it
the first time. That's normal for free programs.
- macOS 15 (Sequoia) or later: try to open it, close the warning and go
to System Settings > Privacy & Security. Near the bottom you'll see
"PS5 Directo was blocked": click "Open Anyway".
- macOS 14 or earlier: right-click (or Control-click) the app > Open >
Open.
- If it still won't open, in Terminal:
xattr -dr com.apple.quarantine "/Applications/PS5 Directo.app"
NOTES
- Works on Apple Silicon (M1, M2...) and Intel Macs, macOS 11 or later.
Python and 7-Zip are included.
- For the window without tabs it uses Chrome, Edge or Brave if you have
one; otherwise it opens in Safari.
- Torrents need aria2: install Homebrew (brew.sh), then
"brew install aria2".
- It updates itself from the program, like on Windows.
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
# Lanzador de "PS5 Directo.app" (va en Contents/MacOS/PS5 Directo).
# - Python incluido para Apple Silicon (python-arm64) e Intel (python-x86_64), en Contents/Resources.
# - 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"
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
[ -z "$2" ] && return 0
[ "$1" = "$2" ] && return 1
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)" = "$1" ]
}
BUNDLED="$(ver "$RES/app/app.py")"
COPIED="$(ver "$APP/app.py")"
if [ ! -f "$APP/app.py" ] || newer "$BUNDLED" "$COPIED"; then
log "copio la version $BUNDLED (habia: ${COPIED:-ninguna})"
rm -rf "$APP.nuevo"
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
# 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). 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# Abre PS5 Directo en Linux (y en Mac, si lo usas desde la carpeta).
# Usa el Python que viene incluido (carpeta python/) o, si no esta, el python3 del sistema.
# Puedes pasarle archivos: ./ps5directo.sh juego.rar
DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR" || exit 1
# 7-Zip incluido (carpeta bin/), para "Descomprimir en el PC"
[ -d "$DIR/bin" ] && PATH="$DIR/bin:$PATH" && export PATH
if [ -x "$DIR/python/bin/python3" ]; then
PY="$DIR/python/bin/python3"
elif command -v python3 >/dev/null 2>&1; then
PY="python3"
else
MSG="No encuentro Python 3. Instálalo (por ejemplo: sudo apt install python3) o baja la versión de PS5 Directo para Linux, que ya lo trae."
command -v zenity >/dev/null 2>&1 && zenity --error --title="PS5 Directo" --text="$MSG" 2>/dev/null
echo "$MSG" >&2
exit 1
fi
export PYTHONUNBUFFERED=1
# Abierto desde el menu (sin terminal): el registro va al mismo archivo que en Windows
if [ ! -t 1 ]; then
mkdir -p "$HOME/.ps5directo"
exec "$PY" "$DIR/app.py" "$@" >> "$HOME/.ps5directo/ps5directo.log" 2>&1
fi
exec "$PY" "$DIR/app.py" "$@"
+259 -74
View File
@@ -323,13 +323,29 @@ tr:hover .dots{opacity:1}
.bkkeep{display:flex;align-items:center;gap:8px;font-size:13.5px}.bkkeep input{width:64px} .bkkeep{display:flex;align-items:center;gap:8px;font-size:13.5px}.bkkeep input{width:64px}
.bkprog{margin:8px 0 12px} .bkprog{margin:8px 0 12px}
.mdgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-top:10px} .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{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 img{width:100%;height:100%;object-fit:cover;display:block}
.mdt .vd{font-size:34px;color:#cfd8ea} .mdt .vd{font-size:34px;color:#fff;position:relative;text-shadow:0 2px 10px rgba(0,0,0,.8)}
.mdt img.cv{position:absolute;inset:0;opacity:.5}
.mdt .hb{position:absolute;top:7px;right:7px;font-size:10px;font-weight:700;padding:1px 6px;border-radius:6px;background:rgba(0,0,0,.65);color:#ffd36b;letter-spacing:.04em}
.mdt .nm{position:absolute;left:0;right:0;bottom:0;padding:3px 7px;font-size:11px;background:linear-gradient(transparent,rgba(0,0,0,.75));color:#dfe6f2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .mdt .nm{position:absolute;left:0;right:0;bottom:0;padding:3px 7px;font-size:11px;background:linear-gradient(transparent,rgba(0,0,0,.75));color:#dfe6f2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mdt input{position:absolute;top:7px;left:7px;width:18px;height:18px} .mdt input{position:absolute;top:7px;left:7px;width:18px;height:18px}
.mdt.on{outline:2px solid #5b9dff;outline-offset:-2px} .mdt.on{outline:2px solid #5b9dff;outline-offset:-2px}
.colroots{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0 10px} .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}
#mdConvBox{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--panel);margin:8px 0;font-size:13px}
#mdConvBox .bar{flex:1 1 160px;min-width:120px}#mdConvBox .err{flex-basis:100%}
.rsd-g{font-weight:600;margin:12px 0 4px;font-size:13px}
.rsd-row{display:flex;gap:10px;align-items:flex-start;padding:6px 4px;margin:0;border-bottom:1px solid var(--line);font-size:13px;color:var(--text)}
.rsd-row input{width:auto;flex:none;margin-top:3px}
.rsd-row>div{flex:1;min-width:0;overflow-wrap:anywhere}.rsd-row .num{flex:none;font-variant-numeric:tabular-nums;font-weight:600}
.rsd-row.off{opacity:.6}
.rsd-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px}
.colroot{display:inline-flex;align-items:center;gap:6px;padding:4px 6px 4px 11px;border-radius:999px;border:1px solid var(--line);font-size:13px;background:var(--panel)} .colroot{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} .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} .lcrow{display:flex;gap:9px;align-items:flex-start;padding:7px 0;border-bottom:1px solid var(--line);font-size:13.5px}
@@ -357,6 +373,10 @@ tr:hover .dots{opacity:1}
.gupd{display:block;width:fit-content;margin:2px 0 6px;font-size:12.5px;color:#8fb6ff;text-decoration:none;border-bottom:1px dashed rgba(143,182,255,.45)} .gupd{display:block;width:fit-content;margin:2px 0 6px;font-size:12.5px;color:#8fb6ff;text-decoration:none;border-bottom:1px dashed rgba(143,182,255,.45)}
.gupd:hover{color:#b8d0ff;border-bottom-style:solid} .gupd:hover{color:#b8d0ff;border-bottom-style:solid}
.gcov img{width:100%;height:100%;object-fit:cover;display:block} .gcov img{width:100%;height:100%;object-fit:cover;display:block}
.gcov .sizeb{position:absolute;right:7px;bottom:7px;font-size:11px;font-weight:700;padding:2px 7px;border-radius:7px;background:rgba(0,0,0,.68);color:#e8eef8;letter-spacing:.02em}
.gcov .dlveil{position:absolute;left:0;right:0;top:0;height:calc(100% - var(--p,0%));backdrop-filter:grayscale(1) brightness(.42);-webkit-backdrop-filter:grayscale(1) brightness(.42);transition:height .8s ease;pointer-events:none}
.gcov .dlpct{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font:800 26px/1 system-ui,sans-serif;color:#fff;text-shadow:0 2px 12px rgba(0,0,0,.9);pointer-events:none}
.gcard.dling{border-color:#4a7bd6}
.gcov .ph{font:700 22px/1.1 "Segoe UI",system-ui,sans-serif;color:#9fb3d1;letter-spacing:.04em;text-align:center;padding:10px} .gcov .ph{font:700 22px/1.1 "Segoe UI",system-ui,sans-serif;color:#9fb3d1;letter-spacing:.04em;text-align:center;padding:10px}
.gcov .fmtb{position:absolute;left:8px;top:8px;font-size:11px;padding:2px 7px;border-radius:6px;background:rgba(10,12,16,.78);color:#dbe7ff;border:1px solid rgba(255,255,255,.12)} .gcov .fmtb{position:absolute;left:8px;top:8px;font-size:11px;padding:2px 7px;border-radius:6px;background:rgba(10,12,16,.78);color:#dbe7ff;border:1px solid rgba(255,255,255,.12)}
.gbody{padding:10px 11px 11px;display:flex;flex-direction:column;gap:5px;flex:1} .gbody{padding:10px 11px 11px;display:flex;flex-direction:column;gap:5px;flex:1}
@@ -380,6 +400,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} .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{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-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{background:#fff;border-radius:10px;padding:8px;display:inline-block;margin-top:10px}
.qr svg{display:block;width:220px;height:220px} .qr svg{display:block;width:220px;height:220px}
.qr.qrs svg{width:120px;height:120px} .qr.qrs svg{width:120px;height:120px}
@@ -438,6 +462,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
.su-row>div{flex:1;min-width:0} .su-row>div{flex:1;min-width:0}
.su-legal{font-size:12.5px;color:var(--muted);border-left:3px solid var(--line);padding-left:10px} .su-legal{font-size:12.5px;color:var(--muted);border-left:3px solid var(--line);padding-left:10px}
@media(max-width:600px){.lib-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}.gact button{font-size:11.5px;padding:4px 5px}} @media(max-width:600px){.lib-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}.gact button{font-size:11.5px;padding:4px 5px}}
body.notwin .winonly{display:none!important}
</style> </style>
</head> </head>
<body> <body>
@@ -448,8 +473,8 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<div class="sub">Envía archivos a tu consola por la red local, sin pasar por el disco del PC</div> <div class="sub">Envía archivos a tu consola por la red local, sin pasar por el disco del PC</div>
</div> </div>
<div class="spacer"></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="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 cargar payloads en la PS5"><span class="dot"></span>BFpilot</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> <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 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> <button class="small" id="btnLang" data-noi18n title="Idioma / Language"></button>
@@ -479,7 +504,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<h2>Nueva transferencia</h2> <h2>Nueva transferencia</h2>
<label for="lines">Enlaces directos (uno por línea). Pega todas las partes juntas: .part1.rar, .part2.rar… o .001, .002…</label> <label for="lines">Enlaces directos (uno por línea). Pega todas las partes juntas: .part1.rar, .part2.rar… o .001, .002…</label>
<textarea id="lines" placeholder="https://servidor/archivo.part1.rar&#10;https://servidor/archivo.part2.rar&#10;&#10;También vale la ruta de un archivo o carpeta de tu PC (D:\Descargas\archivo.rar), un enlace ftp:// de tu NAS o un torrent (magnet o .torrent)"></textarea> <textarea id="lines" placeholder="https://servidor/archivo.part1.rar&#10;https://servidor/archivo.part2.rar&#10;&#10;También vale la ruta de un archivo o carpeta de tu PC (D:\Descargas\archivo.rar), un enlace ftp:// de tu NAS o un torrent (magnet o .torrent)"></textarea>
<div class="drophint" id="drophint">Archivos de tu PC: suéltalos en esta página y los busco en tu PC, o arrástralos sobre el <b>icono de PS5 Directo</b>, o clic derecho → <b>Enviar a → PS5 Directo</b>.</div> <div class="drophint" id="drophint">Archivos de tu PC: suéltalos en esta página y los busco en tu PC, o arrástralos sobre el <b>icono de PS5 Directo</b><span class="winonly">, o clic derecho → <b>Enviar a → PS5 Directo</b></span>.</div>
<div class="pickrow"> <div class="pickrow">
<button class="small" id="btnPickFiles">Elegir archivos…</button> <button class="small" id="btnPickFiles">Elegir archivos…</button>
<button class="small" id="btnPickFolder">Elegir carpeta…</button> <button class="small" id="btnPickFolder">Elegir carpeta…</button>
@@ -553,6 +578,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<button class="primary small" id="libScan">Actualizar lista</button></div> <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> <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="libGU" hidden></div>
<div id="libDl" hidden></div>
<div class="lib-tools"> <div class="lib-tools">
<input id="libSearch" placeholder="Buscar por nombre o Title ID…"> <input id="libSearch" placeholder="Buscar por nombre o Title ID…">
<select id="libFilter"> <select id="libFilter">
@@ -582,7 +608,8 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<div class="fm-head"><h2>En tu PC</h2><div class="spacer"></div> <div class="fm-head"><h2>En tu PC</h2><div class="spacer"></div>
<button class="small" id="colAdd">Añadir carpeta…</button> <button class="small" id="colAdd">Añadir carpeta…</button>
<button class="primary small" id="colScan">Volver a leer</button></div> <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="colroots" id="colRoots"></div>
<div class="lib-sum" id="colSum"></div> <div class="lib-sum" id="colSum"></div>
<div class="lib-grid" id="colGrid"></div> <div class="lib-grid" id="colGrid"></div>
@@ -654,6 +681,12 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<button class="small" id="spCancel" style="margin-top:8px">Cancelar</button></div> <button class="small" id="spCancel" style="margin-top:8px">Cancelar</button></div>
<div id="spOut"></div> <div id="spOut"></div>
</section> </section>
<section class="card" id="rsdCard">
<div class="fm-head"><h2>🧹 Limpiar restos</h2><div class="spacer"></div>
<button class="primary small" id="rsdScan">Buscar restos</button></div>
<p class="hint">Envíos y bajadas que se quedaron a medias, carpetas temporales y copias que ya no hacen falta, en la consola y en el PC. Te enseño lo que ocupa cada cosa y no se borra nada sin que lo confirmes.</p>
<div id="rsdOut"></div>
</section>
<section class="card" id="mrCard"> <section class="card" id="mrCard">
<div class="fm-head"><h2>Hacer sitio</h2></div> <div class="fm-head"><h2>Hacer sitio</h2></div>
<p class="hint">Di cuánto necesitas liberar y te propongo qué borrar: primero restos y envíos a medias, luego copias repetidas de lo mismo y luego instaladores .pkg. Las aplicaciones solo salen como opción, sin marcar. Nada se borra sin que lo confirmes.</p> <p class="hint">Di cuánto necesitas liberar y te propongo qué borrar: primero restos y envíos a medias, luego copias repetidas de lo mismo y luego instaladores .pkg. Las aplicaciones solo salen como opción, sin marcar. Nada se borra sin que lo confirmes.</p>
@@ -692,10 +725,13 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<section class="card"> <section class="card">
<div class="fm-head"><h2>Capturas y vídeos</h2><div class="spacer"></div> <div class="fm-head"><h2>Capturas y vídeos</h2><div class="spacer"></div>
<button class="small" id="mdSel">Seleccionar todo</button> <button class="small" id="mdSel">Seleccionar todo</button>
<button class="small" id="mdConv" disabled title="Vídeos a MP4, y sin HDR para que no se vean lavados">🎬 Convertir a MP4</button>
<button class="small" id="mdDown" disabled>Bajar al PC</button> <button class="small" id="mdDown" disabled>Bajar al PC</button>
<button class="primary small" id="mdLoad">Actualizar</button></div> <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> <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 class="hint" id="mdSum"></div>
<div id="mdTips"></div>
<div id="mdConvBox" hidden></div>
<div class="mdgrid" id="mdGrid"></div> <div class="mdgrid" id="mdGrid"></div>
</section> </section>
</main> </main>
@@ -780,6 +816,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<div class="wl-c" style="--c:#ec6fb4"><b>💾 Copias de seguridad</b><span>Tus partidas y ajustes de la consola guardados en el PC por si algo falla.</span></div> <div class="wl-c" style="--c:#ec6fb4"><b>💾 Copias de seguridad</b><span>Tus partidas y ajustes de la consola guardados en el PC por si algo falla.</span></div>
<div class="wl-c" style="--c:#45c3dc"><b>📱 Móvil y navegador</b><span>Contrólalo desde el móvil con un QR y manda enlaces desde el navegador sin copiar y pegar.</span></div> <div class="wl-c" style="--c:#45c3dc"><b>📱 Móvil y navegador</b><span>Contrólalo desde el móvil con un QR y manda enlaces desde el navegador sin copiar y pegar.</span></div>
</div> </div>
<p class="wl-note macbeta" hidden><b>🍎 Versión de Mac en pruebas:</b> 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!</p>
<p class="wl-note"><b>No es solo un programa para subir archivos:</b> une descarga, conversión, envío y gestión de la consola en uno. Tampoco instala ni cambia nada del sistema: solo mueve los archivos que tú eliges, y cada uno lo usa bajo su responsabilidad.</p> <p class="wl-note"><b>No es solo un programa para subir archivos:</b> une descarga, conversión, envío y gestión de la consola en uno. Tampoco instala ni cambia nada del sistema: solo mueve los archivos que tú eliges, y cada uno lo usa bajo su responsabilidad.</p>
<div class="actions"><button class="small" id="wlTour">Ver la guía de uso</button><div class="spacer"></div><button class="primary" id="wlOk">Empezar</button></div> <div class="actions"><button class="small" id="wlTour">Ver la guía de uso</button><div class="spacer"></div><button class="primary" id="wlOk">Empezar</button></div>
</div> </div>
@@ -850,11 +887,14 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
</div> </div>
</dialog> </dialog>
<dialog id="dlgPayloads"> <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"> <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="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> <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> </div>
</dialog> </dialog>
<dialog id="dlgDiag"> <dialog id="dlgDiag">
@@ -932,8 +972,8 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<p class="hint">Alternativa a BFpilot: 7-Zip descomprime en el PC sobre la marcha y la app manda los archivos ya sueltos, comprobando cada uno con su CRC. No escribe nada descomprimido en el PC y en la PS5 solo ocupa el contenido final (sin el RAR al lado). A cambio, por el Wi-Fi viaja todo sin comprimir (puede ser 3-4 veces más) y los enlaces se bajan antes al PC (hace falta sitio para el RAR).</p> <p class="hint">Alternativa a BFpilot: 7-Zip descomprime en el PC sobre la marcha y la app manda los archivos ya sueltos, comprobando cada uno con su CRC. No escribe nada descomprimido en el PC y en la PS5 solo ocupa el contenido final (sin el RAR al lado). A cambio, por el Wi-Fi viaja todo sin comprimir (puede ser 3-4 veces más) y los enlaces se bajan antes al PC (hace falta sitio para el RAR).</p>
<div class="row tri"> <div class="row tri">
<div><label>Por defecto</label><select id="s_extract_where"><option value="ps5">En la PS5 (BFpilot)</option><option value="pc">En el PC (7-Zip)</option></select></div> <div><label>Por defecto</label><select id="s_extract_where"><option value="ps5">En la PS5 (BFpilot)</option><option value="pc">En el PC (7-Zip)</option></select></div>
<div><label>Carpeta del PC para bajar los RAR</label><input id="s_pc_temp_dir" type="text" placeholder="Descargas\PS5 Directo (temporal)"></div> <div><label>Carpeta del PC para bajar los RAR</label><input id="s_pc_temp_dir" type="text" placeholder="Descargas › PS5 Directo (temporal)"></div>
<div><label>Ruta de 7z.exe (vacío = buscarlo)</label><input id="s_sevenzip_path" type="text" placeholder="C:\Program Files\7-Zip\7z.exe"></div> <div><label>Ruta de 7-Zip (vacío = buscarlo)</label><input id="s_sevenzip_path" type="text" placeholder="C:\Program Files\7-Zip\7z.exe"></div>
</div> </div>
</div> </div>
<div style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px"> <div style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px">
@@ -947,7 +987,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
</div> </div>
<div style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px"> <div style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px">
<b>Herramientas</b> <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>
<div class="actions"> <div class="actions">
<button class="primary" id="btnSave">Guardar</button> <button class="primary" id="btnSave">Guardar</button>
@@ -980,7 +1020,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
</div> </div>
<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:10px"> <div style="margin-top:14px;border-top:1px solid var(--line);padding-top:10px">
<b>Limpieza de la PS5</b> <b>Limpieza de la PS5</b>
<p class="hint">Busca en /data/ps5directo restos que ya no usa ninguna transferencia de la cola (partes de trabajos quitados, descompresiones a medias...). Te enseña cuánto ocupa cada cosa antes de borrar.</p> <p class="hint">Busca envíos y bajadas a medias y restos que ya no usa ninguna transferencia de la cola, en la consola y en el PC. Te enseña cuánto ocupa cada cosa antes de borrar. También está en Espacio.</p>
<button id="btnScan" class="small">Buscar restos</button> <button id="btnScan" class="small">Buscar restos</button>
<div id="cleanOut" style="margin-top:8px"></div> <div id="cleanOut" style="margin-top:8px"></div>
</div> </div>
@@ -991,9 +1031,9 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script> <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_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 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,{"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,{"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\"."}); 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\"."});
@@ -1033,10 +1073,25 @@ Object.assign(I18N_EXACT,{"Ver actualizaciones": "See updates", "Abre en el nave
Object.assign(I18N_EXACT,{"Instalado en la consola": "Installed on the console", "instalado": "installed", "Instalado por la consola: se desinstala desde su menú, no desde aquí.": "Installed by the console: uninstall it from its menu, not from here.", "Se gestiona desde la consola": "Managed from the console"}); Object.assign(I18N_EXACT,{"Instalado en la consola": "Installed on the console", "instalado": "installed", "Instalado por la consola: se desinstala desde su menú, no desde aquí.": "Installed by the console: uninstall it from its menu, not from here.", "Se gestiona desde la consola": "Managed from the console"});
Object.assign(I18N_EXACT,{"PS5 Game Updater no está bien colocado:": "PS5 Game Updater isn't in the right place:", "Colocarlo bien": "Put it in place", "PS5 Game Updater está abierto en la consola: busca e instala las actualizaciones oficiales de lo instalado.": "PS5 Game Updater is open on the console: it finds and installs official updates for what's installed.", "Tienes PS5 Game Updater en la consola. Ábrelo allí para poder usarlo desde aquí.": "You have PS5 Game Updater on the console. Open it there to use it from here.", "Abrir Game Updater": "Open Game Updater", "Colocado en /data/homebrew/PPSA99440": "Placed in /data/homebrew/PPSA99440", "Abre PS5 Game Updater en la consola y vuelve a probar.": "Open PS5 Game Updater on the console and try again."}); Object.assign(I18N_EXACT,{"PS5 Game Updater no está bien colocado:": "PS5 Game Updater isn't in the right place:", "Colocarlo bien": "Put it in place", "PS5 Game Updater está abierto en la consola: busca e instala las actualizaciones oficiales de lo instalado.": "PS5 Game Updater is open on the console: it finds and installs official updates for what's installed.", "Tienes PS5 Game Updater en la consola. Ábrelo allí para poder usarlo desde aquí.": "You have PS5 Game Updater on the console. Open it there to use it from here.", "Abrir Game Updater": "Open Game Updater", "Colocado en /data/homebrew/PPSA99440": "Placed in /data/homebrew/PPSA99440", "Abre PS5 Game Updater en la consola y vuelve a probar.": "Open PS5 Game Updater on the console and try again."});
I18N_RX.push(['^Está en ([\\s\\S]+?), pero tiene que estar en ([\\s\\S]+?) para que funcione\\.$','It is in {0}, but it has to be in {1} to work.'],['^La carpeta ([\\s\\S]+?) no tiene sce_sys: está incompleta\\.$','The folder {0} has no sce_sys: it is incomplete.']); I18N_RX.push(['^Está en ([\\s\\S]+?), pero tiene que estar en ([\\s\\S]+?) para que funcione\\.$','It is in {0}, but it has to be in {1} to work.'],['^La carpeta ([\\s\\S]+?) no tiene sce_sys: está incompleta\\.$','The folder {0} has no sce_sys: it is incomplete.']);
Object.assign(I18N_EXACT,{"Copia de seguridad": "Backup", "Capturas": "Captures", "En la consola": "On the console", "En tu PC": "On your PC", "Abrir carpeta": "Open folder", "Hacer copia ahora": "Back up now", "Guarda en tu PC lo importante de la consola por si algo falla (un cuelgue, un reinicio o tener que volver a empezar): partidas guardadas, ajustes de ShadowMountPlus y de los payloads, y la base de datos de aplicaciones. Las partidas se guardan tal cual, cifradas: solo se pueden restaurar en esta misma consola y con el mismo usuario. Lo que hay en /data/homebrew no hace falta copiarlo: no se pierde.": "Saves what matters on the console to your PC in case something goes wrong (a crash, a reboot or having to start over): saved data, ShadowMountPlus and payload settings, and the app database. Saved data is stored as is, encrypted: it can only be restored to this same console and user. What's in /data/homebrew doesn't need backing up: it isn't lost.", "Partidas guardadas (PS5 y PS4)": "Saved data (PS5 and PS4)", "Ajustes de ShadowMountPlus y de los payloads": "ShadowMountPlus and payload settings", "Base de datos de aplicaciones (solo se copia, no se restaura)": "App database (backed up only, never restored)", "Hacer una copia sola cada día (si la consola está encendida)": "Back up automatically every day (if the console is on)", "Guardar las últimas": "Keep the last", "copias": "backups", "Cancelar": "Cancel", "Fecha": "Date", "Contenido": "Contents", "Tamaño": "Size", "Capturas y vídeos": "Captures and videos", "Seleccionar todo": "Select all", "Bajar al PC": "Download to PC", "Actualizar": "Refresh", "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.": "Screenshots and videos saved on the console. Click one to see it full size; tick the ones you want and \"Download to PC\" saves them to Downloads\\PS5 Directo.", "Añadir carpeta…": "Add folder…", "Volver a leer": "Read again", "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).": "Choose the folders on your PC or NAS where you keep content for the console (network paths like \\\\NAS\\folder work too). It marks what's already on the console and what isn't, and you send it with one click (with the format and destination chosen in Transfers).", "Diagnóstico": "Diagnosis", "Cerrar": "Close", "Últimas líneas del registro": "Last log lines", "El informe es un .zip con el registro, la versión y los ajustes (sin contraseñas). Pásaselo a quien te ayude.": "The report is a .zip with the log, version and settings (no passwords). Send it to whoever is helping you.", "Copiar resumen": "Copy summary", "Guardar informe (.zip)": "Save report (.zip)", "Bienvenido a PS5 Directo": "Welcome to PS5 Directo", "Un gestor de archivos para tu PS5 que descarga, convierte y envía en un solo paso.": "A file manager for your PS5 that downloads, converts and sends in a single step.", "⚡ Descarga y envía a la vez": "⚡ Downloads and sends at once", "Pegas un enlace y va directo a la consola mientras se descarga, sin llenar el disco del PC.": "Paste a link and it goes straight to the console while it downloads, without filling your PC's disk.", "🔄 Convierte por el camino": "🔄 Converts on the way", "Carpeta, .exfat, LZ4, .ffpfsc… dentro del mismo trabajo, sin usar otro programa.": "Folder, .exfat, LZ4, .ffpfsc… within the same job, no other program needed.", "📦 Descomprime y coloca": "📦 Extracts and places", "RAR, 7z, ZIP y torrents, también por partes y con contraseña. Lo deja listo en /data/homebrew.": "RAR, 7z, ZIP and torrents, also split and password-protected. Leaves it ready in /data/homebrew.", "📚 Biblioteca": "📚 Library", "Lo que hay en la consola con portadas, versión y firmware, lo instalado de PS4 y PS5, y lo que tienes en el PC.": "What's on the console with covers, version and firmware, installed PS4 and PS5 content, and what you have on your PC.", "💾 Copias de seguridad": "💾 Backups", "Tus partidas y ajustes de la consola guardados en el PC por si algo falla.": "Your saved data and console settings kept on your PC in case something goes wrong.", "📱 Móvil y navegador": "📱 Phone and browser", "Contrólalo desde el móvil con un QR y manda enlaces desde el navegador sin copiar y pegar.": "Control it from your phone with a QR code and send links from your browser without copy and paste.", "No es solo un programa para subir archivos:": "It's not just an upload tool:", "une descarga, conversión, envío y gestión de la consola en uno. Tampoco instala ni cambia nada del sistema: solo mueve los archivos que tú eliges, y cada uno lo usa bajo su responsabilidad.": "it combines downloadinLine truncated Object.assign(I18N_EXACT,{"Copia de seguridad": "Backup", "Capturas": "Captures", "En la consola": "On the console", "En tu PC": "On your PC", "Abrir carpeta": "Open folder", "Hacer copia ahora": "Back up now", "Guarda en tu PC lo importante de la consola por si algo falla (un cuelgue, un reinicio o tener que volver a empezar): partidas guardadas, ajustes de ShadowMountPlus y de los payloads, y la base de datos de aplicaciones. Las partidas se guardan tal cual, cifradas: solo se pueden restaurar en esta misma consola y con el mismo usuario. Lo que hay en /data/homebrew no hace falta copiarlo: no se pierde.": "Saves what matters on the console to your PC in case something goes wrong (a crash, a reboot or having to start over): saved data, ShadowMountPlus and payload settings, and the app database. Saved data is stored as is, encrypted: it can only be restored to this same console and user. What's in /data/homebrew doesn't need backing up: it isn't lost.", "Partidas guardadas (PS5 y PS4)": "Saved data (PS5 and PS4)", "Ajustes de ShadowMountPlus y de los payloads": "ShadowMountPlus and payload settings", "Base de datos de aplicaciones (solo se copia, no se restaura)": "App database (backed up only, never restored)", "Hacer una copia sola cada día (si la consola está encendida)": "Back up automatically every day (if the console is on)", "Guardar las últimas": "Keep the last", "copias": "backups", "Cancelar": "Cancel", "Fecha": "Date", "Contenido": "Contents", "Tamaño": "Size", "Capturas y vídeos": "Captures and videos", "Seleccionar todo": "Select all", "Bajar al PC": "Download to PC", "Actualizar": "Refresh", "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.": "Screenshots and videos saved on the console. Click one to see it full size; tick the ones you want and \"Download to PC\" saves them to Downloads › PS5 Directo.", "Añadir carpeta…": "Add folder…", "Volver a leer": "Read again", "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).": "Choose the folders on your PC or NAS where you keep content for the console (network paths like \\\\NAS\\folder work too). It marks what's already on the console and what isn't, and you send it with one click (with the format and destination chosen in Transfers).", "Diagnóstico": "Diagnosis", "Cerrar": "Close", "Últimas líneas del registro": "Last log lines", "El informe es un .zip con el registro, la versión y los ajustes (sin contraseñas). Pásaselo a quien te ayude.": "The report is a .zip with the log, version and settings (no passwords). Send it to whoever is helping you.", "Copiar resumen": "Copy summary", "Guardar informe (.zip)": "Save report (.zip)", "Bienvenido a PS5 Directo": "Welcome to PS5 Directo", "Un gestor de archivos para tu PS5 que descarga, convierte y envía en un solo paso.": "A file manager for your PS5 that downloads, converts and sends in a single step.", "⚡ Descarga y envía a la vez": "⚡ Downloads and sends at once", "Pegas un enlace y va directo a la consola mientras se descarga, sin llenar el disco del PC.": "Paste a link and it goes straight to the console while it downloads, without filling your PC's disk.", "🔄 Convierte por el camino": "🔄 Converts on the way", "Carpeta, .exfat, LZ4, .ffpfsc… dentro del mismo trabajo, sin usar otro programa.": "Folder, .exfat, LZ4, .ffpfsc… within the same job, no other program needed.", "📦 Descomprime y coloca": "📦 Extracts and places", "RAR, 7z, ZIP y torrents, también por partes y con contraseña. Lo deja listo en /data/homebrew.": "RAR, 7z, ZIP and torrents, also split and password-protected. Leaves it ready in /data/homebrew.", "📚 Biblioteca": "📚 Library", "Lo que hay en la consola con portadas, versión y firmware, lo instalado de PS4 y PS5, y lo que tienes en el PC.": "What's on the console with covers, version and firmware, installed PS4 and PS5 content, and what you have on your PC.", "💾 Copias de seguridad": "💾 Backups", "Tus partidas y ajustes de la consola guardados en el PC por si algo falla.": "Your saved data and console settings kept on your PC in case something goes wrong.", "📱 Móvil y navegador": "📱 Phone and browser", "Contrólalo desde el móvil con un QR y manda enlaces desde el navegador sin copiar y pegar.": "Control it from your phone with a QR code and send links from your browser without copy and paste.", "No es solo un programa para subir archivos:": "It's not just an upload tool:", "une descarga, conversión, envío y gestión de la consola en uno. Tampoco instala ni cambia nada del sistema: solo mueve los archivos que tú eliges, y cada uno lo usa bajo su responsabilidad.": "it combines downLine truncated
Object.assign(I18N_EXACT,{"Todo lo que tienes en la consola con su portada, versión y formato, también lo instalado de PS4 y PS5. Te avisa de lo que no arranca en tu firmware y de lo repetido. En «En tu PC» eliges tus carpetas del PC o del NAS y ves lo que falta en la consola para enviarlo con un clic.": "Everything on the console with cover, version and format, including installed PS4 and PS5 content. It flags what won't launch on your firmware and duplicates. In \"On your PC\" you pick your PC or NAS folders and see what's missing on the console to send it with one click.", "Guarda en el PC tus partidas y los ajustes de la consola por si algo falla, a mano o una vez al día, y restaura las partidas cuando lo necesites.": "Saves your saved data and console settings to the PC in case something goes wrong, manually or once a day, and restores saved data when you need it.", "Mira las capturas y los vídeos de la consola y bájalos al PC.": "Browse the console's screenshots and videos and download them to the PC.", "Si una transferencia da error, pulsa «Diagnóstico» en ella: te dice qué ha pasado y cómo arreglarlo, y guarda un informe para pasar a quien te ayude. En Ajustes, «Revisión completa» revisa la conexión.": "If a transfer fails, click \"Diagnosis\" on it: it tells you what happened and how to fix it, and saves a report to send to whoever helps you. In Settings, \"Full check\" checks the connection."}); Object.assign(I18N_EXACT,{"Todo lo que tienes en la consola con su portada, versión y formato, también lo instalado de PS4 y PS5. Te avisa de lo que no arranca en tu firmware y de lo repetido. En «En tu PC» eliges tus carpetas del PC o del NAS y ves lo que falta en la consola para enviarlo con un clic.": "Everything on the console with cover, version and format, including installed PS4 and PS5 content. It flags what won't launch on your firmware and duplicates. In \"On your PC\" you pick your PC or NAS folders and see what's missing on the console to send it with one click.", "Guarda en el PC tus partidas y los ajustes de la consola por si algo falla, a mano o una vez al día, y restaura las partidas cuando lo necesites.": "Saves your saved data and console settings to the PC in case something goes wrong, manually or once a day, and restores saved data when you need it.", "Mira las capturas y los vídeos de la consola y bájalos al PC.": "Browse the console's screenshots and videos and download them to the PC.", "Si una transferencia da error, pulsa «Diagnóstico» en ella: te dice qué ha pasado y cómo arreglarlo, y guarda un informe para pasar a quien te ayude. En Ajustes, «Revisión completa» revisa la conexión.": "If a transfer fails, click \"Diagnosis\" on it: it tells you what happened and how to fix it, and saves a report to send to whoever helps you. In Settings, \"Full check\" checks the connection."});
Object.assign(I18N_EXACT,{"Comprobar arranque": "Launch check", "Comprobar": "Check", "Si no abre o se cierra al arrancar: revisa la carpeta y busca registros y capturas": "If it won't open or closes on launch: checks the folder and looks for logs and captures", "Revisa la carpeta en la consola y busca registros y capturas de los últimos minutos. Truco: si al abrirla sale un error en la tele, pulsa el botón de captura del mando antes de cerrarlo; la captura aparecerá aquí.": "Checks the folder on the console and looks for logs and captures from the last few minutes. Tip: if an error shows on the TV when opening it, press the controller's capture button before closing it; the capture will show up here.", "Código de error que salió en la tele (si lo hubo)": "Error code shown on the TV (if any)", "Volver a comprobar": "Check again", "Revisando…": "Checking…", "No encuentro nada raro en la carpeta.": "Nothing odd found in the folder.", "Últimas capturas de la consola (por si salió el error en pantalla):": "Latest console captures (in case the error was on screen):", "Registro reciente": "Recent log", "No hay registros (.log) de los últimos 30 minutos en la consola. Si acabas de probar a abrirla, es que no ha dejado ninguno.": "No logs (.log) from the last 30 minutes on the console. If you just tried to open it, it didn't leave any.", "No conozco ese código. Guárdalo en el informe para que lo vea quien te ayude.": "I don't know that code. Save it in the report so whoever helps you can see it.", "Tiene eboot.bin (el ejecutable principal).": "Has eboot.bin (the main executable).", "eboot.bin no tiene permiso de ejecución.": "eboot.bin isn't executable.", "Pulsa «Permisos» en la tarjeta (pone 777).": "Click \"Permissions\" on the card (sets 777).", "Falta eboot.bin: la aplicación no puede arrancar.": "eboot.bin is missing: the app can't start.", "Vuelve a enviarla: está incompleta.": "Send it again: it's incomplete.", "Tiene sce_sys/param.json.": "Has sce_sys/param.json.", "Falta sce_sys/param.json: la consola no sabe qué es.": "sce_sys/param.json is missing: the console doesn't know what it is.", "Falta la portada (icon0.png). No impide arrancar.": "The cover (icon0.png) is missing. It doesn't stop it starting.", "Necesitas una versión con backport para tu firmware.": "You need a version with a backport for your firmware.", "Lleva backport (carpeta fakelib).": "It has a backport (fakelib folder).", "Está empaquetada en LZ4 pero falta ampr_assets.index.": "It's LZ4-packed but ampr_assets.index is missing.", "Vuelve a convertirla o envíala sin convertir.": "Convert it again or send it unconverted.", "LZ4: falta ampr_emu.index.": "LZ4: ampr_emu.index is missing.", "Si no arranca, vuelve a convertirla o envíala sin convertir.": "If it won't start, convert it again or send it unconverted.", "LZ4: no lleva perfil .runtime (usa los valores por defecto). Si se cierra al arrancar, es una causa habitual.": "LZ4: no .runtime profile (uses defaults). If it closes on launch, this is a common cause.", "Prueba a enviarla sin convertir, como carpeta o .exfat.": "Try sending it unconverted, as a folder or .exfat.", "Vuelve a enviarla.": "Send it again.", "LZ4: lleva el emulador AMPR en su fakelib.": "LZ4: the AMPR emulator is in its fakelib.", "LZ4: no encuentro el emulador AMPR (libSceAmpr) en fakelib.": "LZ4: can't find the AMPR emulator (libSceAmpr) in fakelib.", "Sin él los datos empaquetados no se pueden leer. Vuelve a convertirla.": "Without it the packed data can't be read. Convert it again.", "Si se cierra al arrancar, prueba a apartar esa carpeta.": "If it closes on launch, try moving that folder aside.", "Deja solo una.": "Keep only one.", "Si no aparece en el menú, prueba a enviarla como carpeta o .exfat.": "If it doesn't show in the menu, try sending it as a folder or .exfat."}); Object.assign(I18N_EXACT,{"Comprobar arranque": "Launch check", "Comprobar": "Check", "Si no abre o se cierra al arrancar: revisa la carpeta y busca registros y capturas": "If it won't open or closes on launch: checks the folder and looks for logs and captures", "Revisa la carpeta en la consola y busca registros y capturas de los últimos minutos. Truco: si al abrirla sale un error en la tele, pulsa el botón de captura del mando antes de cerrarlo; la captura aparecerá aquí.": "Checks the folder on the console and looks for logs and captures from the last few minutes. Tip: if an error shows on the TV when opening it, press the controller's capture button before closing it; the capture will show up here.", "Código de error que salió en la tele (si lo hubo)": "Error code shown on the TV (if any)", "Volver a comprobar": "Check again", "Revisando…": "Checking…", "No encuentro nada raro en la carpeta.": "Nothing odd found in the folder.", "Últimas capturas de la consola (por si salió el error en pantalla):": "Latest console captures (in case the error was on screen):", "Registro reciente": "Recent log", "No hay registros (.log) de los últimos 30 minutos en la consola. Si acabas de probar a abrirla, es que no ha dejado ninguno.": "No logs (.log) from the last 30 minutes on the console. If you just tried to open it, it didn't leave any.", "No conozco ese código. Guárdalo en el informe para que lo vea quien te ayude.": "I don't know that code. Save it in the report so whoever helps you can see it.", "Tiene eboot.bin (el ejecutable principal).": "Has eboot.bin (the main executable).", "eboot.bin no tiene permiso de ejecución.": "eboot.bin isn't executable.", "Pulsa «Permisos» en la tarjeta (pone 777).": "Click \"Permissions\" on the card (sets 777).", "Falta eboot.bin: la aplicación no puede arrancar.": "eboot.bin is missing: the app can't start.", "Vuelve a enviarla: está incompleta.": "Send it again: it's incomplete.", "Tiene sce_sys/param.json.": "Has sce_sys/param.json.", "Falta sce_sys/param.json: la consola no sabe qué es.": "sce_sys/param.json is missing: the console doesn't know what it is.", "Falta la portada (icon0.png). No impide arrancar.": "The cover (icon0.png) is missing. It doesn't stop it starting.", "Necesitas una versión con backport para tu firmware.": "You need a version with a backport for your firmware.", "Lleva backport (carpeta fakelib).": "It has a backport (fakelib folder).", "Está empaquetada en LZ4 pero falta ampr_assets.index.": "It's LZ4-packed but ampr_assets.index is missing.", "Vuelve a convertirla o envíala sin convertir.": "Convert it again or send it unconverted.", "LZ4: falta ampr_emu.index.": "LZ4: ampr_emu.index is missing.", "Si no arranca, vuelve a convertirla o envíala sin convertir.": "If it won't start, convert it again or send it unconverted.", "LZ4: no lleva perfil .runtime (usa los valores por defecto). Si se cierra al arrancar, es una causa habitual.": "LZ4: no .runtime profile (uses defaults). If it closes on launch, this is a common cause.", "Prueba a enviarla sin convertir, como carpeta o .exfat.": "Try sending it unconverted, as a folder or .exfat.", "Vuelve a enviarla.": "Send it again.", "LZ4: lleva el emulador AMPR en su fakelib.": "LZ4: the AMPR emulator is in its fakelib.", "LZ4: no encuentro el emulador AMPR (libSceAmpr) en fakelib.": "LZ4: can't find the AMPR emulator (libSceAmpr) in fakelib.", "Sin él los datos empaquetados no se pueden leer. Vuelve a convertirla.": "Without it the packed data can't be read. Convert it again.", "Si se cierra al arrancar, prueba a apartar esa carpeta.": "If it closes on launch, try moving that folder aside.", "Deja solo una.": "Keep only one.", "Si no aparece en el menú, prueba a enviarla como carpeta o .exfat.": "If it doesn't show in the menu, try sending it as a folder or .exfat."});
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).']); 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). Para los que ya tienes, márcalos y pulsa «Convertir a MP4».": "🎬 Your clips are WEBM: Telegram, WhatsApp and many websites don't play them directly. To record new ones as MP4, on the PS5 go to Settings → Captures and Broadcasts → Captures → Video Clip Format → File Type and choose \"Most Compatible (MP4)\" (with the recording resolution at 1920 × 1080; 4K only offers WebM). For the ones you already have, tick them and click \"Convert to MP4\".", "🌗 ¿Se ven lavados (sin contraste) al compartirlos? Pasa con las capturas y clips grabados en HDR, que el móvil, Telegram o las webs no saben mostrar. Para que se vean bien en todas partes, en la PS5 ve a Ajustes → Capturas y emisiones → Capturas y desactiva «HDR» en el formato de las capturas de pantalla y de los videoclips. Los vídeos que ya tienes en HDR, «Convertir a MP4» los deja con colores normales.": "🌗 Do they look washed out (no contrast) when you share them? It happens with screenshots and clips recorded in HDR, which phones, Telegram and websites can't display properly. To make them look right everywhere, on the PS5 go to Settings → Captures and Broadcasts → Captures and turn off \"HDR\" in the screenshot and video clip formats. For HDR videos you already have, \"Convert to MP4\" gives them normal colours.", "Entendido": "Got it"});
Object.assign(I18N_EXACT,{"Lo que ocupa en la consola": "Size on the console"});
Object.assign(I18N_EXACT,{"Busca envíos y bajadas a medias y restos que ya no usa ninguna transferencia de la cola, en la consola y en el PC. Te enseña cuánto ocupa cada cosa antes de borrar. También está en Espacio.": "Finds half-finished sends and downloads and leftovers no transfer in the queue uses anymore, on the console and on the PC. It shows how much each one takes before deleting. It's also in Space.", "Vídeos a MP4, y sin HDR para que no se vean lavados": "Videos to MP4, without HDR so they don't look washed out", "🎬 Convertir a MP4": "🎬 Convert to MP4", "Convertir a MP4": "Convert to MP4", "Convertir a MP4 ({0})": "Convert to MP4 ({0})", "Marca algún vídeo": "Tick a video first", "Elige algún vídeo": "Choose a video", "Para convertir hace falta FFmpeg, un programa gratuito de vídeo. Se descarga una sola vez (33 MB, de GitHub) y se guarda en la carpeta de PS5 Directo. ¿Instalarlo y convertir?": "Converting needs FFmpeg, a free video program. It's downloaded only once (33 MB, from GitHub) and kept in the PS5 Directo folder. Install it and convert?", "Para convertir hace falta FFmpeg. Instálalo con Homebrew (brew.sh): brew install ffmpeg": "Converting needs FFmpeg. Install it with Homebrew (brew.sh): brew install ffmpeg", "Para convertir hace falta FFmpeg. Instálalo (por ejemplo: sudo apt install ffmpeg) y vuelve a probar.": "Converting needs FFmpeg. Install it (for example: sudo apt install ffmpeg) and try again.", "Convirtiendo {0} de {1}": "Converting {0} of {1}", "Bajando de la consola": "Downloading from the console", "Convirtiendo a MP4": "Converting to MP4", "Convirtiendo a MP4 y quitando el HDR (tarda un poco más)": "Converting to MP4 and removing HDR (takes a bit longer)", "Instalando el convertidor (FFmpeg, 33 MB, solo la primera vez)": "Installing the converter (FFmpeg, 33 MB, first time only)", "Conversión cancelada": "Conversion cancelled", "{0} vídeos en MP4 en Descargas › PS5 Directo › Capturas, listos para compartir.": "{0} videos as MP4 in Downloads › PS5 Directo › Capturas, ready to share.", "No se pudo convertir": "Couldn't convert", "Ya se está convirtiendo": "Already converting", "🧹 Limpiar restos": "🧹 Clean up leftovers", "Buscar restos": "Find leftovers", "Envíos y bajadas que se quedaron a medias, carpetas temporales y copias que ya no hacen falta, en la consola y en el PC. Te enseño lo que ocupa cada cosa y no se borra nada sin que lo confirmes.": "Sends and downloads that were left half-finished, temporary folders and copies that aren't needed anymore, on the console and on the PC. I show you how much each one takes and nothing is deleted without your confirmation.", "Buscando restos en la consola y en el PC… (puede tardar unos segundos)": "Looking for leftovers on the console and on the PC… (may take a few seconds)", "No hay restos: todo limpio.": "No leftovers: all clean.", "En la consola": "On the console", "En el PC": "On the PC", "No se puede borrar ahora: {0}": "Can't delete it now: {0}", "Borrar marcados ({0})": "Delete ticked ({0})", "¿Borrar {0} elementos ({1})? No se puede deshacer.": "Delete {0} items ({1})? This can't be undone.", "Borrando…": "Deleting…", "Liberados {0}": "{0} freed", "algo se está bajando ahora": "something is downloading right now", "Copias guardadas para ver las capturas (se vuelven a bajar al abrirlas)": "Copies kept to view captures (downloaded again when you open them)", "BFpilot no responde: en la consola solo miro los archivos a medias, no la carpeta de trabajo.": "BFpilot isn't responding: on the console I only check half-sent files, not the work folder.", "No es una aplicación completa. Márcala solo si sabes que es un envío que se cortó.": "It isn't a complete app. Tick it only if you know it's a send that got cut off."});
I18N_RX.push(...[["^Bajada a medias \\(Bajado de la Biblioteca\\): ([\\s\\S]*?)$", "Half-finished download (from the Library): {0}"], ["^Bajada a medias \\(Descargas\\): ([\\s\\S]*?)$", "Half-finished download (Downloads): {0}"], ["^Bajada a medias \\(Capturas\\): ([\\s\\S]*?)$", "Half-finished file (Captures): {0}"], ["^Temporal de transferencias por el PC: ([\\s\\S]*?)$", "Temporary files of transfers through the PC: {0}"], ["^Descompresión a medias en el PC: ([\\s\\S]*?)$", "Half-finished extraction on the PC: {0}"], ["^Envío a medias: ([\\s\\S]*?)$", "Half-sent: {0}"], ["^Carpeta sin sce_sys \\(¿envío cortado\\?\\): ([\\s\\S]*?)$", "Folder without sce_sys (send cut off?): {0}"], ["^puede que lo use «([\\s\\S]*?)» de la cola$", "\"{0}\" in the queue may be using it"], ["^lo usa «([\\s\\S]*?)» de la cola$", "used by \"{0}\" in the queue"], ["^No pude revisar la consola: ([\\s\\S]*?)$", "Couldn't check the console: {0}"], ["^FFmpeg falló: ([\\s\\S]*?)$", "FFmpeg failed: {0}"], ["^No pude instalar FFmpeg \\(([\\s\\S]*?)\\)\\. Puedes bajarlo de gyan\\.dev\\/ffmpeg y copiar ffmpeg\\.exe en ([\\s\\S]*?)$", "Couldn't install FFmpeg ({0}). You can download it from gyan.dev/ffmpeg and copy ffmpeg.exe into {1}"]]);
Object.assign(I18N_EXACT,{"macOS no deja a PS5 Directo usar la red local. Ve a Ajustes del Sistema → Privacidad y seguridad → Red local, activa «PS5 Directo» (o «python3») y vuelve a abrir el programa.": "macOS isn't letting PS5 Directo use the local network. Go to System Settings → Privacy & Security → Local Network, turn on \"PS5 Directo\" (or \"python3\") and open the program again."});
I18N_RX.push(...[["^macOS\\ no\\ deja\\ a\\ PS5\\ Directo\\ usar\\ la\\ red\\ local\\.\\ Ve\\ a\\ Ajustes\\ del\\ Sistema\\ →\\ Privacidad\\ y\\ seguridad\\ →\\ Red\\ local,\\ activa\\ «PS5\\ Directo»\\ \\(o\\ «python3»\\)\\ y\\ vuelve\\ a\\ abrir\\ el\\ programa\\. \\(([\\s\\S]*?)\\)$", "macOS isn't letting PS5 Directo use the local network. Go to System Settings → Privacy & Security → Local Network, turn on \"PS5 Directo\" (or \"python3\") and open the program again. ({0})"]]);
// --- idioma: español (original) o inglés. Traduce el texto de la página y los mensajes del programa. // --- 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){} 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'})(); return (navigator.language||'es').toLowerCase().startsWith('es')?'es':'en'})();
@@ -1149,7 +1204,7 @@ function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt
const ORDER=['comprobando','enviando','en cola para descomprimir','extrayendo','colocando','hecho']; 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'}; 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){ function pcStepsHtml(j){
// Modo "descomprimir en el PC": bajar al PC -> descomprimir y enviar -> colocar -> listo // Modo "descomprimir en el PC": bajar al PC -> descomprimir y enviar -> colocar -> listo
// Con PS5 Dump Forge: bajar al PC -> convertir -> enviar -> listo // Con PS5 Dump Forge: bajar al PC -> convertir -> enviar -> listo
@@ -1309,7 +1364,7 @@ async function checkConn(){try{paintConn(await api('/api/test',{}))}catch(e){}}
let refreshFails=0, refreshing=false; let refreshFails=0, refreshing=false;
async function refresh(){ async function refresh(){
if(refreshing) return; refreshing=true; if(refreshing) return; refreshing=true;
try{const s=await api('/api/state');refreshFails=0;state=s;paintBell(s.unread||0);if(s.version){$('ver').textContent='v'+s.version;window.titleBase='PS5 Directo v'+s.version;if(!window.titleTimer)document.title=window.titleBase}if(s.inbox>0)takeInbox();if(!$('dest').dataset.touched)$('dest').value=s.settings.dest||'/data/homebrew';if(!$('where').dataset.touched)$('where').value=s.settings.extract_where==='pc'?'pc':'ps5';if(!$('fmt').dataset.touched){const f=s.settings.pc_format;$('fmt').value=(f&&f!=='folder'&&($('where').value==='pc'||f==='plain'))?f:'';fmtNote()}render();renderSched();applyRemote();paintFw()} try{const s=await api('/api/state');refreshFails=0;state=s;paintBell(s.unread||0);if(s.os&&s.os!=='nt')document.body.classList.add('notwin');if(s.version){$('ver').textContent='v'+s.version;window.titleBase='PS5 Directo v'+s.version;if(!window.titleTimer)document.title=window.titleBase}if(s.inbox>0)takeInbox();if(!$('dest').dataset.touched)$('dest').value=s.settings.dest||'/data/homebrew';if(!$('where').dataset.touched)$('where').value=s.settings.extract_where==='pc'?'pc':'ps5';if(!$('fmt').dataset.touched){const f=s.settings.pc_format;$('fmt').value=(f&&f!=='folder'&&($('where').value==='pc'||f==='plain'))?f:'';fmtNote()}render();renderSched();applyRemote();paintFw()}
catch(e){ if(++refreshFails>=5) $('jobs').innerHTML=`<div class="empty">No conecto con el programa. Cierra esta ventana y abre PS5 Directo otra vez. (${esc(e.message)})</div>`} catch(e){ if(++refreshFails>=5) $('jobs').innerHTML=`<div class="empty">No conecto con el programa. Cierra esta ventana y abre PS5 Directo otra vez. (${esc(e.message)})</div>`}
finally{ refreshing=false } finally{ refreshing=false }
fmAfterUploads(); fmAfterUploads();
@@ -1491,24 +1546,8 @@ $('btnPerms').onclick=async()=>{
`${esc(x.path)}: ${x.changed?`<b>arreglados ${x.changed}</b> de ${x.seen}${x.eboot.length?` (eboot.bin tenía ${esc(x.eboot[0])})`:''}`:'ya estaba bien'}`).join('<br>'); `${esc(x.path)}: ${x.changed?`<b>arreglados ${x.changed}</b> de ${x.seen}${x.eboot.length?` (eboot.bin tenía ${esc(x.eboot[0])})`:''}`:'ya estaba bien'}`).join('<br>');
}catch(e){o.textContent=e.message} }catch(e){o.textContent=e.message}
}; };
// --- limpieza // --- limpieza: ahora en Espacio
let cleanItems=[]; $('btnScan').onclick=()=>{ try{$('dlg').close()}catch(e){} showTab('Space'); rsdScan(); setTimeout(()=>$('rsdCard').scrollIntoView({behavior:'smooth',block:'start'}),50) };
$('btnScan').onclick=async()=>{
const o=$('cleanOut'); o.textContent='Buscando… (puede tardar unos segundos)';
try{const r=await api('/api/cleanup/scan',{}); cleanItems=r.items;
if(!cleanItems.length){o.textContent='No hay nada que limpiar.';return}
o.innerHTML=cleanItems.map((it,i)=>`<label class="chk"><input type="checkbox" data-i="${i}" ${it.in_use?'':'checked'}><div>${esc(it.label)} — <b>${human(it.size)}</b>${it.in_use?`<div class="hint">Lo usa "${esc(it.in_use)}" de la cola: si lo borras, esa transferencia tendrá que enviarse otra vez.</div>`:''}</div></label>`).join('')
+`<button id="btnClean" class="small danger" style="margin-top:8px">Borrar seleccionados</button>`;
$('btnClean').onclick=async()=>{
const sel=[...o.querySelectorAll('input[data-i]:checked')].map(x=>cleanItems[+x.dataset.i]);
if(!sel.length) return;
const tot=sel.reduce((a,b)=>a+(b.size||0),0);
if(!confirm(`¿Borrar ${sel.length} elemento(s) de la PS5 (${human(tot)})? No se puede deshacer.`)) return;
o.textContent='Borrando…';
try{const d=await api('/api/cleanup/delete',{paths:sel.map(x=>x.path)}); o.textContent=`Borrado: ${d.deleted.length} elemento(s), ${human(tot)} liberados.`; checkConn()}catch(e){o.textContent=e.message}
};
}catch(e){o.textContent=e.message}
};
// --- destinos segun unidades montadas // --- destinos segun unidades montadas
async function loadDests(){try{const r=await api('/api/places',{});$('destList').innerHTML=r.dests.map(d=>`<option value="${esc(d)}">`).join('')}catch(e){}} async function loadDests(){try{const r=await api('/api/places',{});$('destList').innerHTML=r.dests.map(d=>`<option value="${esc(d)}">`).join('')}catch(e){}}
loadDests(); setInterval(loadDests, 60000); loadDests(); setInterval(loadDests, 60000);
@@ -1665,7 +1704,7 @@ function afterUpdate(v){
// ===================== bienvenida ===================== // ===================== bienvenida =====================
let welcomeNext=null; let welcomeNext=null;
function showWelcome(next){ welcomeNext=next||null; $('dlgWelcome').showModal(); } function showWelcome(next){ welcomeNext=next||null; document.querySelectorAll('.macbeta').forEach(e=>e.hidden=(state.platform!=='darwin')); $('dlgWelcome').showModal(); }
function welcomeDone(){ $('dlgWelcome').close(); api('/api/settings',{welcome_seen:true}).catch(()=>{}); if(state.settings) state.settings.welcome_seen=true; const n=welcomeNext; welcomeNext=null; if(n) n(); } function welcomeDone(){ $('dlgWelcome').close(); api('/api/settings',{welcome_seen:true}).catch(()=>{}); if(state.settings) state.settings.welcome_seen=true; const n=welcomeNext; welcomeNext=null; if(n) n(); }
$('wlOk').onclick=welcomeDone; $('wlOk').onclick=welcomeDone;
$('wlTour').onclick=()=>{ welcomeNext=null; welcomeDone(); startTour(); }; $('wlTour').onclick=()=>{ welcomeNext=null; welcomeDone(); startTour(); };
@@ -1740,40 +1779,116 @@ $('rsGo').onclick=async()=>{
// ===================== capturas y videos ===================== // ===================== capturas y videos =====================
let md={items:[],sel:new Set(),loaded:false}; let md={items:[],sel:new Set(),loaded:false};
function mdEnter(){ if(!md.loaded) mdLoad(); } function mdEnter(){ if(!md.loaded) mdLoad(); mdConvWatch(); }
async function mdLoad(){ async function mdLoad(){
md.loaded=true; $('mdSum').textContent=tr('Leyendo la consola…'); $('mdGrid').innerHTML=''; md.loaded=true; $('mdSum').textContent=tr('Leyendo la consola…'); $('mdGrid').innerHTML='';
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} 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). Para los que ya tienes, márcalos y pulsa «Convertir a MP4».')]);
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. Los vídeos que ya tienes en HDR, «Convertir a MP4» los deja con colores normales.')]);
$('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(){ function mdPaint(){
mdTipsPaint();
const n=md.items.length, v=md.items.filter(x=>x.video).length; 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.'); $('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)}`; $('mdGrid').innerHTML=md.items.map((x,i)=>{const u=`/api/media/file?p=${encodeURIComponent(x.path)}&t=${encodeURIComponent(TOKEN)}&v=1`;
return `<div class="mdt ${md.sel.has(i)?'on':''}" data-i="${i}" title="${esc(x.name)} · ${human(x.size)}">${x.video?'<span class="vd">▶</span>':`<img loading="lazy" src="${u}" alt="">`}<input type="checkbox" ${md.sel.has(i)?'checked':''}><span class="nm" data-noi18n>${esc(x.folder)} · ${esc(x.name)}</span></div>`}).join(''); const cov=x.tid?`<img class="cv" loading="lazy" src="/api/cover?p=${encodeURIComponent('/user/appmeta/'+x.tid)}&t=${encodeURIComponent(TOKEN)}" alt="" onerror="this.remove()">`:'';
$('mdDown').disabled=!md.sel.size; $('mdDown').textContent=md.sel.size?tr('Bajar al PC ({0})').replace('{0}',md.sel.size):tr('Bajar al PC'); const pic=x.video?cov+'<span class="vd">▶</span>':`<img loading="lazy" src="${u}" alt="" onerror="this.replaceWith(document.createTextNode('🖼'))">`;
const label=(x.app||x.tid||x.folder)+(x.when?' · '+mdWhen(x.when):'');
return `<div class="mdt ${md.sel.has(i)?'on':''}" data-i="${i}" title="${esc(x.name)} · ${human(x.size)}">${pic}${x.hdr?'<span class="hb">HDR</span>':''}<input type="checkbox" ${md.sel.has(i)?'checked':''}><span class="nm" data-noi18n>${esc(label)}</span></div>`}).join('');
mdBtns();
} }
$('mdGrid').addEventListener('click',e=>{const t=e.target.closest('.mdt'); if(!t) return; const i=+t.dataset.i; $('mdGrid').addEventListener('click',e=>{const t=e.target.closest('.mdt'); if(!t) return; const i=+t.dataset.i;
if(e.target.tagName==='INPUT'){ e.target.checked?md.sel.add(i):md.sel.delete(i); t.classList.toggle('on',e.target.checked); $('mdDown').disabled=!md.sel.size; $('mdDown').textContent=md.sel.size?tr('Bajar al PC ({0})').replace('{0}',md.sel.size):tr('Bajar al PC'); return } if(e.target.tagName==='INPUT'){ e.target.checked?md.sel.add(i):md.sel.delete(i); t.classList.toggle('on',e.target.checked); mdBtns(); return }
window.open(`/api/media/file?p=${encodeURIComponent(md.items[i].path)}&t=${encodeURIComponent(TOKEN)}`,'_blank','noopener');}); window.open(`/api/media/file?p=${encodeURIComponent(md.items[i].path)}&t=${encodeURIComponent(TOKEN)}&v=1`,'_blank','noopener');});
$('mdSel').onclick=()=>{ if(md.sel.size===md.items.length) md.sel.clear(); else md.items.forEach((_,i)=>md.sel.add(i)); mdPaint(); }; $('mdSel').onclick=()=>{ if(md.sel.size===md.items.length) md.sel.clear(); else md.items.forEach((_,i)=>md.sel.add(i)); mdPaint(); };
$('mdLoad').onclick=mdLoad; $('mdLoad').onclick=mdLoad;
$('mdDown').onclick=async()=>{ const paths=[...md.sel].map(i=>md.items[i].path); $('mdDown').onclick=async()=>{ const paths=[...md.sel].map(i=>md.items[i].path);
try{const r=await api('/api/fm/download',{paths}); if(!r.ok) throw new Error(r.error); toast(tr('Bajando {0} archivos a Descargas\\PS5 Directo. El progreso sale en «Archivos de la consola».').replace('{0}',paths.length))}catch(e){toast(e.message)} }; try{const r=await api('/api/fm/download',{paths}); if(!r.ok) throw new Error(r.error); toast(tr('Bajando {0} archivos a Descargas › PS5 Directo. El progreso sale en «Archivos de la consola».').replace('{0}',paths.length))}catch(e){toast(e.message)} };
// --- convertir a MP4 para compartir
let mdConv={poll:null};
function mdSelVideos(){ return [...md.sel].map(i=>md.items[i]).filter(x=>x&&x.video) }
function mdBtns(){ const n=md.sel.size, v=mdSelVideos().length;
$('mdDown').disabled=!n; $('mdDown').textContent=n?tr('Bajar al PC ({0})').replace('{0}',n):tr('Bajar al PC');
$('mdConv').disabled=!v||mdConv.poll!=null; $('mdConv').textContent='🎬 '+(v?tr('Convertir a MP4 ({0})').replace('{0}',v):tr('Convertir a MP4')); }
$('mdConv').onclick=async()=>{ const vids=mdSelVideos(); if(!vids.length){toast(tr('Marca algún vídeo'));return}
let t; try{t=await api('/api/media/tools',{}); if(!t.ok) throw new Error(t.error)}catch(e){toast(e.message);return}
if(!t.ffmpeg){ if(!t.can_install){ alert(tr(t.hint)); return }
if(!confirm(tr('Para convertir hace falta FFmpeg, un programa gratuito de vídeo. Se descarga una sola vez (33 MB, de GitHub) y se guarda en la carpeta de PS5 Directo. ¿Instalarlo y convertir?'))) return }
try{const r=await api('/api/media/convert',{paths:vids.map(x=>x.path)}); if(!r.ok) throw new Error(r.error); mdConvWatch()}catch(e){toast(e.message)} };
function mdConvWatch(){ if(mdConv.poll) return; mdConv.poll=setInterval(mdConvPoll,700); mdConvPoll(); mdBtns() }
async function mdConvPoll(){
let r; try{r=await api('/api/media/convstate',{})}catch(e){return} const c=r.conv, box=$('mdConvBox'); if(!c) return;
const errs=(c.errors||[]).length?`<div class="err jderr">${c.errors.map(e=>esc(tr(e))).join('<br>')}</div>`:'';
if(c.busy){ box.hidden=false;
box.innerHTML=`<span>🎬 ${esc(tr('Convirtiendo {0} de {1}').replace('{0}',c.i).replace('{1}',c.n))}: <b data-noi18n>${esc(c.current||'')}</b> · ${esc(tr(c.phase||''))} · ${c.pct||0}%</span><div class="bar"><i style="width:${c.pct||0}%"></i></div><button class="small" id="mdConvX">${esc(tr('Cancelar'))}</button>${errs}`;
$('mdConvX').onclick=()=>api('/api/media/convcancel',{}); return }
clearInterval(mdConv.poll); mdConv.poll=null; mdBtns();
if(!c.n){ box.hidden=true; return }
const ok=(c.done||[]).length;
box.hidden=false;
box.innerHTML=`<span>${c.phase==='Cancelado'?'✖ '+esc(tr('Conversión cancelada')):ok?'✔ '+esc(tr('{0} vídeos en MP4 en Descargas › PS5 Directo › Capturas, listos para compartir.').replace('{0}',ok)):'✖ '+esc(tr('No se pudo convertir'))}</span><div class="spacer"></div>${ok?`<button class="small primary" id="mdConvOpen">${esc(tr('Abrir carpeta'))}</button>`:''}<button class="small" id="mdConvClose">${esc(tr('Cerrar'))}</button>${errs}`;
if($('mdConvOpen')) $('mdConvOpen').onclick=()=>api('/api/media/opendir',{});
$('mdConvClose').onclick=()=>{box.hidden=true};
}
// --- limpiar restos (Espacio)
let rsd={items:[]};
async function rsdScan(){
const o=$('rsdOut'); o.innerHTML=`<div class="hint">${esc(tr('Buscando restos en la consola y en el PC… (puede tardar unos segundos)'))}</div>`; $('rsdScan').disabled=true;
try{const r=await api('/api/residue/scan',{}); if(!r.ok) throw new Error(r.error); rsd.items=r.items; rsdPaint(r.warn||[])}catch(e){o.innerHTML=`<div class="jderr">${esc(tr(e.message))}</div>`}
$('rsdScan').disabled=false;
}
function rsdPaint(warn){
const o=$('rsdOut'), it=rsd.items;
const w=(warn||[]).map(x=>`<div class="hint">⚠ ${esc(tr(x))}</div>`).join('');
if(!it.length){ o.innerHTML=w+`<div class="hint">✔ ${esc(tr('No hay restos: todo limpio.'))}</div>`; return }
const row=(x,i)=>`<label class="rsd-row ${x.in_use?'off':''}"><input type="checkbox" data-i="${i}" ${x.checked?'checked':''} ${x.in_use?'disabled':''}><div>${esc(tr(x.label))}<div class="hint mono" data-noi18n>${esc(x.path)}</div>${x.in_use?`<div class="hint">${esc(tr('No se puede borrar ahora: {0}').replace('{0}',tr(x.in_use)))}</div>`:''}${x.note?`<div class="hint">${esc(tr(x.note))}</div>`:''}</div><span class="num">${x.size?esc(human(x.size)):'—'}</span></label>`;
const grp=(where,title)=>{const l=it.map((x,i)=>[x,i]).filter(([x])=>x.where===where); return l.length?`<div class="rsd-g">${esc(tr(title))}</div>`+l.map(([x,i])=>row(x,i)).join(''):''};
o.innerHTML=w+grp('ps5','En la consola')+grp('pc','En el PC')+`<div class="rsd-foot"><button class="danger small" id="rsdDel"></button><span class="hint" id="rsdSum"></span></div>`;
const upd=()=>{const sel=rsdSel(); $('rsdDel').disabled=!sel.length; $('rsdDel').textContent=tr('Borrar marcados ({0})').replace('{0}',human(sel.reduce((a,x)=>a+(x.size||0),0)))};
o.querySelectorAll('input[data-i]').forEach(c=>c.onchange=upd); upd();
$('rsdDel').onclick=async()=>{ const sel=rsdSel(); if(!sel.length) return;
const tot=sel.reduce((a,x)=>a+(x.size||0),0);
if(!confirm(tr('¿Borrar {0} elementos ({1})? No se puede deshacer.').replace('{0}',sel.length).replace('{1}',human(tot)))) return;
$('rsdDel').disabled=true; $('rsdSum').textContent=tr('Borrando…');
try{const r=await api('/api/residue/delete',{ids:sel.map(x=>x.id)}); if(!r.ok) throw new Error(r.error);
toast(tr('Liberados {0}').replace('{0}',human(r.freed||0))); await rsdScan();
if((r.errors||[]).length) $('rsdOut').insertAdjacentHTML('afterbegin',`<div class="jderr">${r.errors.map(e=>esc(tr(e))).join('<br>')}</div>`); checkConn()
}catch(e){$('rsdSum').textContent=tr(e.message)} };
}
function rsdSel(){ return [...$('rsdOut').querySelectorAll('input[data-i]:checked')].map(c=>rsd.items[+c.dataset.i]) }
$('rsdScan').onclick=rsdScan;
// ===================== biblioteca: en tu PC ===================== // ===================== biblioteca: en tu PC =====================
let col={roots:[],items:null,poll:null}; let col={roots:[],items:null,poll:null};
$('libView').addEventListener('click',e=>{const b=e.target.closest('button[data-v]'); if(!b) return; $('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)); $('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();}); 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(){ async function colState(){
let r; try{r=await api('/api/col/state',{})}catch(e){return} 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) } 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) } else { clearInterval(col.poll); col.poll=null; colPaint(); if(r.col.error) toast(r.col.error) }
} }
function colPaintRoots(){ 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()}); $('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)} } async function colScanNow(){ try{await api('/api/col/scan',{}); setTimeout(colState,300)}catch(e){toast(e.message)} }
@@ -1818,7 +1933,7 @@ async function lcRun(){
const ic={ok:'✔',warn:'!',bad:'✖',info:'i'}; const ic={ok:'✔',warn:'!',bad:'✖',info:'i'};
$('lcList').innerHTML=r.checks.map(c=>`<div class="lcrow ${c.level}"><i>${ic[c.level]}</i><div>${esc(tr(c.text))}${c.fix?`<div class="fx">${esc(tr(c.fix))}</div>`:''}</div></div>`).join('')||`<div class="hint">${esc(tr('No encuentro nada raro en la carpeta.'))}</div>`; $('lcList').innerHTML=r.checks.map(c=>`<div class="lcrow ${c.level}"><i>${ic[c.level]}</i><div>${esc(tr(c.text))}${c.fix?`<div class="fx">${esc(tr(c.fix))}</div>`:''}</div></div>`).join('')||`<div class="hint">${esc(tr('No encuentro nada raro en la carpeta.'))}</div>`;
const caps=r.captures||[]; const caps=r.captures||[];
$('lcCaps').innerHTML=caps.length?`<div class="hint" style="width:100%">${esc(tr('Últimas capturas de la consola (por si salió el error en pantalla):'))}</div>`+caps.map(c=>{const u=`/api/media/file?p=${encodeURIComponent(c.path)}&t=${encodeURIComponent(TOKEN)}`;return `<a href="${u}" target="_blank" rel="noopener" title="${esc(c.name)}">${c.video?'▶':`<img src="${u}" alt="">`}</a>`}).join(''):''; $('lcCaps').innerHTML=caps.length?`<div class="hint" style="width:100%">${esc(tr('Últimas capturas de la consola (por si salió el error en pantalla):'))}</div>`+caps.map(c=>{const u=`/api/media/file?p=${encodeURIComponent(c.path)}&t=${encodeURIComponent(TOKEN)}&v=1`;return `<a href="${u}" target="_blank" rel="noopener" title="${esc(c.name)}">${c.video?'▶':`<img src="${u}" alt="">`}</a>`}).join(''):'';
$('lcLogs').innerHTML=(r.logs||[]).length?r.logs.map(l=>`<details><summary>${esc(tr('Registro reciente'))}: <span class="mono" data-noi18n>${esc(l.path)}</span></summary><pre class="log" data-noi18n>${esc(l.tail)}</pre></details>`).join(''):`<div class="hint">${esc(tr('No hay registros (.log) de los últimos 30 minutos en la consola. Si acabas de probar a abrirla, es que no ha dejado ninguno.'))}</div>`; $('lcLogs').innerHTML=(r.logs||[]).length?r.logs.map(l=>`<details><summary>${esc(tr('Registro reciente'))}: <span class="mono" data-noi18n>${esc(l.path)}</span></summary><pre class="log" data-noi18n>${esc(l.tail)}</pre></details>`).join(''):`<div class="hint">${esc(tr('No hay registros (.log) de los últimos 30 minutos en la consola. Si acabas de probar a abrirla, es que no ha dejado ninguno.'))}</div>`;
} }
$('lcClose').onclick=()=>$('dlgLaunch').close(); $('lcClose').onclick=()=>$('dlgLaunch').close();
@@ -1834,14 +1949,16 @@ let curTab='Transfers';
function showTab(t){ function showTab(t){
curTab=t; curTab=t;
['Transfers','Library','Files','Space','Backup','Media','Activity'].forEach(x=>{$('tab'+x).hidden=(x!==t)}); ['Transfers','Library','Files','Space','Backup','Media','Activity'].forEach(x=>{$('tab'+x).hidden=(x!==t)});
if(t==='Backup') bkEnter();
if(t==='Media') mdEnter();
if(t==='Library') libEnter();
document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.tab===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
if(t==='Files'&&!fm.loaded) fmGo(fm.path,true); try{
if(t==='Activity') loadActivity(); if(t==='Backup') bkEnter();
if(t==='Space') spEnter(); if(t==='Media') mdEnter();
if(t==='Library') libEnter();
if(t==='Files'&&!fm.loaded) fmGo(fm.path,true);
if(t==='Activity') loadActivity();
if(t==='Space') spEnter();
}catch(e){ console.error('Pestaña '+t+':', e) }
} }
document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>showTab(b.dataset.tab)); document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>showTab(b.dataset.tab));
@@ -2414,7 +2531,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; 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)}}); 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()}; $('actClear').onclick=async()=>{if(!confirm(tr('¿Borrar todo el historial?')))return; await api('/api/history/clear',{}); loadActivity()};
try{const t=localStorage.getItem('ps5d_tab'); if(t&&t!=='Transfers'&&$('tab'+t)) showTab(t)}catch(e){} // El programa se abre siempre en «Transferencias» (antes volvía a la última pestaña usada y confundía).
</script> </script>
<script> <script>
// ===================== 1.20: formato, programar, payloads, diagnóstico, móvil ===================== // ===================== 1.20: formato, programar, payloads, diagnóstico, móvil =====================
@@ -2542,24 +2659,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)}}; $('scClear').onclick=async()=>{try{await api('/api/schedule',{start:'',after:'none'});$('dlgSched').close();refresh()}catch(e){toast(e.message)}};
// --- payloads // --- payloads
let plSt=null;
async function openPayloads(highlight){ async function openPayloads(highlight){
try{$('dlg').open&&$('dlg').close()}catch(e){} try{$('dlg').open&&$('dlg').close()}catch(e){}
$('plOut').textContent=''; $('plList').innerHTML='<p class="hint">'+esc(tr('Cargando…'))+'</p>'; $('dlgPayloads').showModal(); $('plOut').textContent=''; if(!$('dlgPayloads').open) $('dlgPayloads').showModal();
let r; try{r=await api('/api/payload/list',{})}catch(e){$('plList').textContent=e.message;return} $('plPm').innerHTML='<p class="hint">'+esc(tr('Mirando la consola…'))+'</p>'; $('plList').innerHTML=''; $('plAuto').innerHTML='';
$('plPort').textContent=r.elf_port; $('plAutoStart').checked=!state.settings||state.settings.auto_payloads!==false;
$('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> let r; try{r=await api('/api/console/status',{}); if(!r.ok) throw new Error(r.error)}catch(e){$('plPm').textContent=e.message;return}
<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> plSt=r; plPaint(highlight);
<button class="small ${p.id===highlight?'primary':''}" data-send="${esc(p.id)}">${esc(tr('Enviar a la PS5'))}</button></div>`).join(''); }
function plPaint(highlight){
const r=plSt, pm=r.pm||{};
$('plPm').innerHTML=!r.reachable
?(r.mac_lan?`<div class="pl-box">✖ ${esc(tr(r.mac_lan))}</div>`:`<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; $('plFile').hidden=!!state.remote;
} }
$('plList').addEventListener('click',async e=>{ async function plFix(b){
const b=e.target.closest('[data-send]'); if(!b) return; const o=$('plOut'); const o=$('plOut'); const a=b.dataset.fix;
b.disabled=true; o.textContent=tr('Enviando… (descarga, envío y espera a que arranque: unos segundos)'); document.querySelectorAll('#dlgPayloads [data-fix]').forEach(x=>x.disabled=true);
try{const r=await api('/api/payload/send',{id:b.dataset.send}); 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]);
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:'')); try{const r=await api('/api/console/fix',{id:b.dataset.id,action:a}); if(!r.ok) throw new Error(r.error);
checkConn(); 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);
}catch(err){o.textContent=err.message} finally{b.disabled=false} 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()=>{ $('plFile').onclick=async()=>{
const ps=await pick('files'); if(!ps.length) return; const o=$('plOut'); o.textContent=tr('Enviando…'); 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} 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}
@@ -2718,14 +2868,24 @@ function libRender(){
if(!it.title&&it.param_err) b.push(`<span class="bdg warn" title="${esc(it.param_err)}">${esc(tr('Sin nombre'))}</span>`); if(!it.title&&it.param_err) b.push(`<span class="bdg warn" title="${esc(it.param_err)}">${esc(tr('Sin nombre'))}</span>`);
const meta=[it.tid,it.version].filter(Boolean).join(' · ')||it.name; const meta=[it.tid,it.version].filter(Boolean).join(' · ')||it.name;
const ph=esc((it.tid||it.name||'?').slice(0,9)); const ph=esc((it.tid||it.name||'?').slice(0,9));
return `<div class="gcard ${ok===false?'bad':''}" data-i="${i}"> return `<div class="gcard ${ok===false?'bad':''}" data-i="${i}" data-path="${esc(it.path)}">
<div class="gcov">${cov?`<img loading="lazy" src="${cov}" alt="" data-ph="${ph}" onerror="libImgErr(this)">`:`<div class="ph">${ph}</div>`}<span class="fmtb">${esc(fmt)}</span></div> <div class="gcov">${cov?`<img loading="lazy" src="${cov}" alt="" data-ph="${ph}" onerror="libImgErr(this)">`:`<div class="ph">${ph}</div>`}<span class="fmtb">${esc(fmt)}</span>${it.size?`<span class="sizeb" title="${esc(tr('Lo que ocupa en la consola'))}">${esc(human(it.size))}</span>`:''}</div>
<div class="gbody"><div class="gtitle" data-noi18n title="${esc(it.title||it.name)}">${esc(it.title||it.name)}</div> <div class="gbody"><div class="gtitle" data-noi18n title="${esc(it.title||it.name)}">${esc(it.title||it.name)}</div>
<div class="gmeta" data-noi18n title="${esc(it.path)}">${esc(meta)}${it.size?' · '+esc(human(it.size)):''}</div> <div class="gmeta" data-noi18n title="${esc(it.path)}">${esc(meta)}</div>
<div class="badges">${b.join('')}</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>`:''} ${/^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(''); }).join('');
libDlMark(libDl.busy?libDl.pct:null);
}
function libDlMark(pct){
const c=libDl.path?document.querySelector(`#libGrid .gcard[data-path="${CSS.escape(libDl.path)}"]`):null;
document.querySelectorAll('#libGrid .gcard.dling').forEach(x=>{ if(x!==c||pct==null){x.classList.remove('dling'); x.querySelectorAll('.dlveil,.dlpct').forEach(e=>e.remove())} });
if(!c||pct==null) return;
c.classList.add('dling'); const cov=c.querySelector('.gcov');
let v=cov.querySelector('.dlveil'), t=cov.querySelector('.dlpct');
if(!v){ v=document.createElement('div'); v.className='dlveil'; cov.appendChild(v); t=document.createElement('div'); t.className='dlpct'; cov.appendChild(t) }
v.style.setProperty('--p',pct+'%'); t.textContent=pct+'%';
} }
function updHint(){ function updHint(){
const mine=(state.settings||{}).console_fw; const mine=(state.settings||{}).console_fw;
@@ -2737,6 +2897,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 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; const a=btn.dataset.g;
if(a==='check'){openLaunch(it); return} 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); libDl.path=it.path; libDl.spd=null; libDl.last=null; 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==='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==='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'){ if(a==='del'){
@@ -2747,6 +2909,29 @@ $('libGrid').addEventListener('click',async e=>{
}catch(err){toast(err.message)} }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:'',path:'',busy:false,pct:0,spd:null,last:null};
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); libDl.busy=false; libDlMark(null); box.hidden=true; return }
box.hidden=false;
if(o.busy){
const done=o.bytes_done||0, tot=o.bytes_total||0, pct=tot?Math.min(100,Math.floor(done*100/tot)):0, now=Date.now()/1000;
// velocidad media de los últimos segundos (suavizada para que no baile)
if(libDl.last&&now>libDl.last.t){ const v=Math.max(0,(done-libDl.last.b)/(now-libDl.last.t)); libDl.spd=libDl.spd==null?v:libDl.spd*0.7+v*0.3 }
libDl.last={t:now,b:done}; libDl.busy=true; libDl.pct=pct;
const rest=libDl.spd>1?(tot-done)/libDl.spd:NaN;
box.innerHTML=`<span>⬇ ${esc(tr('Bajando al PC:'))} <b data-noi18n>${esc(libDl.name)}</b> · ${pct}% · ${human(done)} ${esc(tr('de'))} ${human(tot)}${libDl.spd>1?' · <b>'+human(libDl.spd)+'/s</b>':''}${isFinite(rest)&&rest>0?' · '+etaHtml(rest):''}</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(()=>{});
libDlMark(pct); return }
clearInterval(libDl.t); libDl.busy=false; libDlMark(null);
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)); ['libSearch','libFilter','libSort'].forEach(id=>$(id).addEventListener(id==='libSearch'?'input':'change',libRender));
$('libScan').onclick=()=>libScan(false); $('libRefresh').onclick=()=>libScan(true); $('libScan').onclick=()=>libScan(false); $('libRefresh').onclick=()=>libScan(true);
$('libCancel').onclick=()=>api('/api/library/cancel',{}); $('libCancel').onclick=()=>api('/api/library/cancel',{});
@@ -2816,7 +3001,7 @@ function suShow(i){
} }
if(k==='con'){ $('suTitle').textContent=tr('Comprobar la conexión'); 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> 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>`; <div class="actions"><button class="small" id="suRe">Volver a comprobar</button></div>`;
$('suRe').onclick=suCheck; suCheck(); $('suRe').onclick=suCheck; suCheck();
} }