Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9087afc4e3 | ||
|
|
628c3f63e8 | ||
|
|
be8a217875 | ||
|
|
d5de6399e0 | ||
|
|
6fe219eea7 | ||
|
|
75462232fe | ||
|
|
92eefb4d87 | ||
|
|
8f1870aefd | ||
|
|
dd1bd517b4 | ||
|
|
1f4b37acfd | ||
|
|
80bba19d57 | ||
|
|
088c46f548 | ||
|
|
b65a3b34dd | ||
|
|
3e642e0986 | ||
|
|
d8113534ea | ||
|
|
5128f49f8b | ||
|
|
c8a0466854 | ||
|
|
3314a0f1b0 |
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
@@ -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`
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Publica una version: al subir una etiqueta vX.Y.Z (o con "Run workflow" en Actions) crea o rehace
|
||||||
|
# la release de GitHub con:
|
||||||
|
# - 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
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
ver: ${{ steps.v.outputs.ver }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Version de app.py
|
||||||
|
id: v
|
||||||
|
run: echo "ver=$(grep -m1 -oP '^VERSION = "\K[^"]+' app.py)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: La etiqueta tiene que coincidir con la version
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: test "${GITHUB_REF_NAME#v}" = "${{ steps.v.outputs.ver }}"
|
||||||
|
|
||||||
|
- name: Archivos del programa
|
||||||
|
run: |
|
||||||
|
mkdir -p dist/PS5Directo
|
||||||
|
git archive HEAD | tar -x -C dist/PS5Directo
|
||||||
|
rm -rf dist/PS5Directo/.github dist/PS5Directo/installer
|
||||||
|
|
||||||
|
- name: Python incluido
|
||||||
|
run: |
|
||||||
|
PY=3.12.10
|
||||||
|
curl -fsSL -o py.zip "https://www.python.org/ftp/python/$PY/python-$PY-embed-amd64.zip"
|
||||||
|
mkdir -p dist/PS5Directo/python
|
||||||
|
unzip -q py.zip -d dist/PS5Directo/python
|
||||||
|
# que el Python incluido vea la carpeta del programa
|
||||||
|
for f in dist/PS5Directo/python/python*._pth; do printf '..\n' >> "$f"; done
|
||||||
|
|
||||||
|
- name: Zip para Windows
|
||||||
|
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:
|
||||||
|
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
|
||||||
@@ -2,11 +2,15 @@
|
|||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
title PS5 Directo
|
title PS5 Directo
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
where py >nul 2>nul
|
if exist "%~dp0python\python.exe" (
|
||||||
if %errorlevel%==0 (
|
"%~dp0python\python.exe" app.py %*
|
||||||
py -3 app.py %*
|
|
||||||
) else (
|
) else (
|
||||||
python app.py %*
|
where py >nul 2>nul
|
||||||
|
if not errorlevel 1 (
|
||||||
|
py -3 app.py %*
|
||||||
|
) else (
|
||||||
|
python app.py %*
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if %errorlevel%==3 exit /b 0
|
if %errorlevel%==3 exit /b 0
|
||||||
echo.
|
echo.
|
||||||
|
|||||||
@@ -1,7 +1,54 @@
|
|||||||
PS5 DIRECTO 1.18
|
PS5 DIRECTO 1.20.0
|
||||||
================
|
==================
|
||||||
(English: see README_EN.txt)
|
(English: see README_EN.txt)
|
||||||
|
|
||||||
|
EMPIEZA AQUÍ (5 MINUTOS)
|
||||||
|
========================
|
||||||
|
Lo que necesitas antes:
|
||||||
|
- Una PS5 con el jailbreak ya hecho. PS5 Directo no hace el jailbreak.
|
||||||
|
- Un PC con Windows 10 u 11 conectado a la misma red que la PS5.
|
||||||
|
|
||||||
|
Paso 1. Descarga
|
||||||
|
En la página de GitHub del programa, entra en "Releases" y descarga
|
||||||
|
PS5Directo-X.Y.Z-Windows.zip (trae todo, no hay que instalar nada más). Descomprímelo en una carpeta, por ejemplo en Documentos.
|
||||||
|
|
||||||
|
Paso 2. Ábrelo
|
||||||
|
Doble clic en "PS5 Directo.vbs". Se abre en su propia ventana y crea un
|
||||||
|
icono en el escritorio para la próxima vez. Si Windows pregunta si quieres
|
||||||
|
abrir un archivo descargado de internet, pulsa "Abrir".
|
||||||
|
|
||||||
|
Paso 3. En la PS5
|
||||||
|
Carga el jailbreak como siempre y ShadowMountPlus (es lo que hace que lo
|
||||||
|
que envíes aparezca en el menú). Si tu menú de payloads también carga el
|
||||||
|
FTP (ftpsrv) y BFpilot, mejor; si no, PS5 Directo te ayuda en el paso 5.
|
||||||
|
|
||||||
|
Paso 4. La IP de la PS5
|
||||||
|
El asistente que sale al abrir te la pide. En la PS5: Ajustes > Red >
|
||||||
|
Ver estado de la conexión > "Dirección IPv4" (algo como 192.168.1.50).
|
||||||
|
|
||||||
|
Paso 5. Comprobar la conexión
|
||||||
|
El asistente mira si responden el FTP y BFpilot. Si alguno sale en rojo,
|
||||||
|
pulsa "Cargar" a su lado y se envía solo. (Con Y2JB Autoloader eso no
|
||||||
|
funciona: cárgalos desde Payload Manager en la PS5 y pulsa "Volver a
|
||||||
|
comprobar".)
|
||||||
|
|
||||||
|
Paso 6. Envía algo
|
||||||
|
Pega un enlace de descarga directa (o arrastra un archivo de tu PC a la
|
||||||
|
ventana) y pulsa "Añadir a la cola". Cuando ponga "Listo", ya está en el
|
||||||
|
menú de la PS5.
|
||||||
|
|
||||||
|
¿Algo no va?
|
||||||
|
Ajustes > "Revisión completa" dice qué falla y cómo arreglarlo. Los dos
|
||||||
|
puntos de arriba (FTP y BFpilot) en rojo: púlsalos para cargarlos. Puedes
|
||||||
|
repetir el asistente en Ajustes > "Asistente de primeros pasos".
|
||||||
|
|
||||||
|
Aviso: PS5 Directo solo copia archivos. Úsalo con contenido que tengas
|
||||||
|
derecho a usar (tus copias de seguridad y homebrew). Lo que hagas con él
|
||||||
|
es responsabilidad tuya.
|
||||||
|
|
||||||
|
(Lo de abajo es la guía completa: no hace falta leerla para empezar.)
|
||||||
|
|
||||||
|
|
||||||
Pega los enlaces (o arrastra archivos de tu PC) y la herramienta hace todo sola:
|
Pega los enlaces (o arrastra archivos de tu PC) y la herramienta hace todo sola:
|
||||||
|
|
||||||
1. Descarga los archivos y los manda a la PS5 por FTP mientras bajan
|
1. Descarga los archivos y los manda a la PS5 por FTP mientras bajan
|
||||||
@@ -24,13 +71,20 @@ PESTAÑAS
|
|||||||
luego "Pegar aquí" en la carpeta de destino), borrar, bajar al PC
|
luego "Pegar aquí" en la carpeta de destino), borrar, bajar al PC
|
||||||
(a Descargas\PS5 Directo), poner permisos 777 y subir archivos o
|
(a Descargas\PS5 Directo), poner permisos 777 y subir archivos o
|
||||||
carpetas tal cual a la carpeta en la que estás.
|
carpetas tal cual a la carpeta en la que estás.
|
||||||
Copiar, mover, borrar y calcular tamaños necesitan BFpilot; sin él, el
|
Clic derecho en un archivo o en el hueco para el menú; atajos: Supr,
|
||||||
explorador funciona en solo lectura por FTP.
|
F2, Ctrl+C / Ctrl+X / Ctrl+V, Ctrl+A y Retroceso. "Mover a…" y "Copiar
|
||||||
|
a…" abren un selector de carpetas con los destinos recientes. Si al pegar
|
||||||
|
ya existe algo con el mismo nombre, eliges reemplazar/combinar, conservar
|
||||||
|
ambos u omitir. "Extraer…" descomprime en la consola un .rar/.7z/.zip que
|
||||||
|
ya esté allí. A la izquierda tienes las unidades (con su espacio libre) y
|
||||||
|
accesos rápidos. Puedes arrastrar archivos del PC a la carpeta abierta.
|
||||||
|
Sin BFpilot casi todo funciona por FTP (copiar pasa por el PC, más
|
||||||
|
lento); descomprimir y ver el espacio libre sí necesitan BFpilot.
|
||||||
- Espacio: gráfico de quesitos con lo que ocupa sitio en la consola, con
|
- Espacio: gráfico de quesitos con lo que ocupa sitio en la consola, con
|
||||||
el nombre de cada juego y su tamaño. Avisa de juegos repetidos (por
|
el nombre de cada aplicación y su tamaño. Avisa de contenido repetido (por
|
||||||
ejemplo la carpeta y su versión convertida a imagen), envíos a medias y
|
ejemplo la carpeta y su versión convertida a imagen), envíos a medias y
|
||||||
restos, y permite borrarlos desde ahí (pide confirmación). Necesita
|
restos, y permite borrarlos desde ahí (pide confirmación). Necesita
|
||||||
BFpilot. Los juegos instalados desde la propia PS5 se ven pero se borran
|
BFpilot. Lo instalado desde la propia PS5 se ve pero se borra
|
||||||
desde la consola.
|
desde la consola.
|
||||||
- Actividad: historial de lo que se ha terminado o ha fallado.
|
- Actividad: historial de lo que se ha terminado o ha fallado.
|
||||||
|
|
||||||
@@ -65,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:
|
||||||
@@ -100,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.
|
||||||
|
|
||||||
@@ -168,9 +227,355 @@ de quien es la culpa cuando va lento. Tambien hay uno solo PC -> PS5 en
|
|||||||
Ajustes.
|
Ajustes.
|
||||||
|
|
||||||
|
|
||||||
|
BIBLIOTECA
|
||||||
|
----------
|
||||||
|
La pestaña Biblioteca enseña lo que hay en /data/homebrew (y en los USB) con su
|
||||||
|
portada, nombre, Title ID, versión, si es carpeta o imagen (.ffpkg, .ffpfsc,
|
||||||
|
.exfat) y qué firmware pide. Si pones el firmware de tu PS5 en Ajustes
|
||||||
|
(por ejemplo 13.60), marca en rojo los que no arrancarán. También avisa de
|
||||||
|
contenido repetido, de DLC/actualizaciones y de carpetas incompletas. Funciona
|
||||||
|
solo con FTP; con BFpilot además ve cuánto ocupa cada carpeta.
|
||||||
|
El firmware sale de param.json y, si trae backport (carpeta
|
||||||
|
fakelib), de lo que piden sus ejecutables. Al terminar cada transferencia
|
||||||
|
también se comprueba y te avisa si no va a arrancar.
|
||||||
|
|
||||||
|
|
||||||
|
IMÁGENES Y LZ4 (PS5 DUMP FORGE)
|
||||||
|
-------------------------------
|
||||||
|
En "Cómo dejarlo en la PS5" la opción por defecto es «Sin convertir»: se deja
|
||||||
|
tal como viene (un comprimido se descomprime; si ya es una imagen .ffpkg,
|
||||||
|
.exfat o .ffpfsc, o viene en LZ4, se envía sin tocar). Además puedes elegir:
|
||||||
|
- Carpeta normal: siempre acaba en carpeta; si es una imagen, un .fpkg o
|
||||||
|
viene en LZ4, se deshace en el PC.
|
||||||
|
- Imagen .ffpkg: la que recomienda ShadowMountPlus. Velocidad completa.
|
||||||
|
- Imagen .exfat o .ffpfs: también las monta ShadowMountPlus.
|
||||||
|
- Imagen comprimida .ffpfsc: ocupa un 30-60 % menos, pero la consola la lee
|
||||||
|
más despacio (150-250 MB/s) y algunos títulos pueden dar tirones.
|
||||||
|
- Paquete .fpkg: se instala como una app de la tienda. Solo firmware 11.60
|
||||||
|
o inferior (kstuff, fpkg-enable y ppr-patch). Se deja en /data/pkg.
|
||||||
|
Para .fpkg y .ffpfsc puedes elegir la compresión (rápida, equilibrada,
|
||||||
|
mínimo tamaño o sin comprimir).
|
||||||
|
- LZ4 (carpeta o dentro de una .ffpkg): solo títulos AMPR. Experimental: con
|
||||||
|
algunos títulos se cuelga al guardar partida. Si no es AMPR, se
|
||||||
|
manda como carpeta o como .ffpkg normal.
|
||||||
|
Se prepara en el PC con PS5 Dump Forge (github.com/quer3q/ps5-dump-forge, se
|
||||||
|
descarga solo la primera vez) y se envía un único archivo: va más rápido por
|
||||||
|
FTP que miles de archivos pequeños y no tiene el problema de permisos. Hace
|
||||||
|
falta sitio en el PC para la imagen. Las imágenes .ffpkg/.ffpfsc no son
|
||||||
|
paquetes FPKG: no se instalan, ShadowMountPlus las monta (hasta 13.60 con
|
||||||
|
ShadowMountPlus 1.7). Los DLC y actualizaciones se mandan como carpeta.
|
||||||
|
|
||||||
|
|
||||||
|
ANTES DE ENVIAR
|
||||||
|
---------------
|
||||||
|
- Si ya está en la consola (mismo Title ID), la transferencia se
|
||||||
|
para y te pregunta: enviar igualmente, reemplazar la copia que hay o
|
||||||
|
cancelar. Se puede desactivar en Ajustes.
|
||||||
|
- Si pegas el .sfv / .md5 / .sha1 junto con las partes (o están en la misma
|
||||||
|
carpeta del PC), cada parte se comprueba mientras se envía. Si una llega
|
||||||
|
dañada, se borra de la consola y se avisa antes de descomprimir.
|
||||||
|
- Si falta sitio, el error trae un botón "Hacer sitio": en la pestaña Espacio
|
||||||
|
te propone qué borrar (restos, copias repetidas, instaladores .pkg) hasta
|
||||||
|
llegar a lo que necesitas. Nada se borra sin confirmar.
|
||||||
|
|
||||||
|
|
||||||
|
PAYLOADS, DIAGNÓSTICO, MÓVIL Y PROGRAMAR
|
||||||
|
----------------------------------------
|
||||||
|
- Pulsa en los puntos FTP / BFpilot (o Ajustes > Cargar payloads) para
|
||||||
|
mandar ftpsrv o BFpilot a la PS5 por el cargador de ELF (puerto 9021,
|
||||||
|
etaHEN o elfldr). Se descargan de GitHub y se guardan. El cargador de
|
||||||
|
Y2JB Autoloader solo acepta la propia consola: con él usa Payload Manager.
|
||||||
|
- "¿Por qué va lento?" revisa si el PC va por Wi-Fi o cable, la banda y la
|
||||||
|
señal, cables a 100 Mb/s, VPN, redes distintas y el ping a la PS5, y te
|
||||||
|
dice qué cambiar.
|
||||||
|
- El botón del móvil (arriba) enseña un código QR para abrir PS5 Directo
|
||||||
|
desde el móvil en la misma red: pegar enlaces, ver la cola, pausar, ver
|
||||||
|
tu biblioteca. Desde el móvil no se pueden instalar actualizaciones ni
|
||||||
|
cambiar rutas de programas.
|
||||||
|
- "Programar" (en la cola): empezar a una hora y suspender o apagar el PC
|
||||||
|
al terminar (avisa y espera 1 minuto). Mientras hay envíos, el PC no se
|
||||||
|
suspende solo.
|
||||||
|
|
||||||
|
|
||||||
|
QUÉ DESCARGAR
|
||||||
|
-------------
|
||||||
|
- PS5Directo-X.Y.Z-Setup.exe: el instalador. Es el que debes bajar. Lo
|
||||||
|
trae todo (también Python): ábrelo, pulsa "Instalar" y listo.
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
|
cierra al arrancar. Revisa la carpeta (ejecutable, param.json, permisos,
|
||||||
|
firmware, backports y el empaquetado LZ4), muestra los registros (.log) y
|
||||||
|
las capturas de los últimos minutos, explica el código de error que salió
|
||||||
|
en la tele y guarda un informe .zip. Solo lee: no cambia nada.
|
||||||
|
- Ventana de bienvenida la primera vez: resume qué es PS5 Directo y qué hace.
|
||||||
|
- Biblioteca: también lo instalado en la consola (PS4 y PS5), con portada,
|
||||||
|
versión y firmware, y filtro para verlo.
|
||||||
|
- Biblioteca, «En tu PC»: eliges carpetas del PC o del NAS, marca lo que ya
|
||||||
|
está en la consola y lo que no, y lo envías con un clic.
|
||||||
|
- Biblioteca: «Ver actualizaciones» abre en el navegador la lista pública de
|
||||||
|
actualizaciones oficiales de cada título (Prospero Patches). Avisa de que
|
||||||
|
las que piden firmware 14.00 o más no se pueden instalar. El programa no
|
||||||
|
descarga nada.
|
||||||
|
- Biblioteca: si tienes PS5 Game Updater en la consola, un botón lo abre; si
|
||||||
|
su carpeta está mal colocada, lo detecta y la coloca bien.
|
||||||
|
- Biblioteca: las imágenes y las carpetas sin param.json salen con nombre,
|
||||||
|
portada y firmware.
|
||||||
|
- Nueva pestaña «Copia de seguridad»: guarda en el PC las partidas, los
|
||||||
|
ajustes de ShadowMountPlus y de los payloads y la base de datos de
|
||||||
|
aplicaciones, a mano o una vez al día. Las partidas se pueden restaurar en
|
||||||
|
la misma consola (antes guarda las de ahora).
|
||||||
|
- Nueva pestaña «Capturas»: capturas y vídeos de la consola con miniaturas,
|
||||||
|
para verlos y bajarlos al PC.
|
||||||
|
- «Diagnóstico» en las transferencias con error: qué ha pasado, códigos y cómo
|
||||||
|
arreglarlo, y un informe .zip (sin contraseñas) para pasar a quien te ayude.
|
||||||
|
En Ajustes, «Crear informe de fallo».
|
||||||
|
- Torrents: pega un magnet o un .torrent. Se baja primero al PC con aria2 (se
|
||||||
|
descarga solo) y luego se envía, descomprime o convierte. No comparte al
|
||||||
|
terminar y lo borra del PC.
|
||||||
|
- PS5 Dump Forge se actualiza solo (una vez al día como mucho), comprobando
|
||||||
|
antes que la versión nueva funciona; si no, sigue con la anterior.
|
||||||
|
- Las versiones nuevas de PS5 Directo se avisan solas con una ventana y sus
|
||||||
|
novedades.
|
||||||
|
- Avisa si la velocidad hacia la PS5 cae de golpe (suele ser la consola
|
||||||
|
inactiva: pulsa un botón del mando).
|
||||||
|
- «Formato en la PS5» empieza plegado y dice cuánto tarda: sin convertir,
|
||||||
|
descarga y envío a la vez; convirtiendo, 3 pasos. Tarjetas ordenadas por lo
|
||||||
|
más usado y «¿Cuál elijo?» por necesidades.
|
||||||
|
- Firmware de tu PS5 junto al título (se guarda solo) y la tarjeta .fpkg dice
|
||||||
|
si te vale.
|
||||||
|
- En la cola, «Convertir a …» dice a qué formato, y avisa si algo no se pudo
|
||||||
|
convertir.
|
||||||
|
- Arreglado: un comprimido con versión normal y backport no se convertía;
|
||||||
|
ahora separa la que toca.
|
||||||
|
- Arreglado: lo convertido se mezclaba con una carpeta anterior del mismo
|
||||||
|
nombre.
|
||||||
|
- Arreglado: muchas carpetas de la Biblioteca salían sin nombre.
|
||||||
|
|
||||||
|
NOVEDADES 1.20.1
|
||||||
|
- Si no conecta y el PC y la PS5 están en redes distintas (por ejemplo
|
||||||
|
192.168.50.x y 192.168.1.x), lo dice y explica cómo arreglarlo.
|
||||||
|
- Botones «Usar en el móvil» y «Extensión del navegador» más visibles, y el
|
||||||
|
asistente de inicio los ofrece al final.
|
||||||
|
- «Descargar de nuevo» en la cola (terminados, con error o cancelados) y en
|
||||||
|
Actividad: vuelve a empezar desde cero sin tener que pegar el enlace.
|
||||||
|
- Si al colocar ya existe esa carpeta, pregunta (colocar igualmente con otro
|
||||||
|
nombre, reemplazar o cancelar) en vez de dar error.
|
||||||
|
- Arreglado: si el comprimido trae la versión normal y un «backport», ya no
|
||||||
|
da «Ya existe»: coloca la normal y aparta el backport (o lo descarta si tu
|
||||||
|
firmware no lo necesita). Y si en el destino hay un envío anterior a
|
||||||
|
medias, lo sustituye.
|
||||||
|
- «Guía de uso» renovada: explica el convertidor de formatos, cada pestaña,
|
||||||
|
el uso desde el móvil y la extensión del navegador.
|
||||||
|
- La ventana «¿Cuál elijo?» añade velocidades estimadas donde no había datos
|
||||||
|
y textos más claros sobre LZ4 y .ffpfsc.
|
||||||
|
|
||||||
|
NOVEDADES 1.20.0
|
||||||
|
- Asistente de primeros pasos (IP, comprobar la conexión y cargar lo que
|
||||||
|
falta, firmware) y una guía más corta y sencilla.
|
||||||
|
- Pestaña Biblioteca: lo que hay en la consola con portada, versión, formato y
|
||||||
|
el firmware que pide; marca los que no arrancan en tu firmware, repetidos,
|
||||||
|
DLC e incompletos.
|
||||||
|
- Imágenes .ffpkg / .ffpfsc y LZ4 con PS5 Dump Forge (opcional): se preparan
|
||||||
|
en el PC y se envía un solo archivo. Se eligen con tarjetas debajo del
|
||||||
|
destino, con la explicación de cada uno y el espacio que hace falta en el PC;
|
||||||
|
la primera vez avisa de que convertir ocupa sitio en el PC hasta terminar.
|
||||||
|
Si lo descargado ya es una imagen, se envía tal cual o se convierte al
|
||||||
|
formato elegido, y se coloca aunque BFpilot no esté cargado.
|
||||||
|
- Nuevos formatos: carpeta normal, .exfat, .ffpfs y .fpkg (con compresión
|
||||||
|
a elegir). Se aceptan carpetas, .zip/.7z/.rar, imágenes, .fpkg y LZ4, y
|
||||||
|
sin 7-Zip el contenido se saca con PS5 Dump Forge.
|
||||||
|
- Los formatos van agrupados (primero los que ocupan menos) y el botón
|
||||||
|
«¿Cuál elijo?» compara ventajas e inconvenientes de cada uno.
|
||||||
|
- Tiempo que queda (y hora aproximada) al enviar, descomprimir y convertir.
|
||||||
|
- En el móvil, al terminar o fallar algo sale un aviso grande, vibra y pita
|
||||||
|
(con la página abierta).
|
||||||
|
- Biblioteca: marca como LZ4 las carpetas empaquetadas en LZ4 (y filtro LZ4).
|
||||||
|
- Arreglado: al reanudar un envío preguntaba «Ya está en la consola» por lo
|
||||||
|
que ese mismo envío había dejado a medias.
|
||||||
|
- Arreglado: en algunos Chrome de Android la página del móvil salía en
|
||||||
|
blanco.
|
||||||
|
- Abrir en el móvil: la página va comprimida (carga antes por Wi-Fi) y la
|
||||||
|
ventana muestra «Qué le llega del móvil» para saber si llega y si se corta.
|
||||||
|
- Aviso de firmware: pon el de tu PS5 en Ajustes y te avisa de lo que no
|
||||||
|
arrancará (tiene en cuenta el backport).
|
||||||
|
- Si ya está en la consola, pregunta antes de enviarlo (enviar
|
||||||
|
igualmente, reemplazar o cancelar).
|
||||||
|
- Comprobación con .sfv / .md5 / .sha1 mientras se envía.
|
||||||
|
- "Hacer sitio": propone qué borrar cuando falta espacio.
|
||||||
|
- Cargar ftpsrv y BFpilot en la PS5 desde el programa (puerto 9021).
|
||||||
|
- "¿Por qué va lento?": diagnóstico de la red del PC.
|
||||||
|
- Abrir desde el móvil con un código QR.
|
||||||
|
- Programar la cola, suspender o apagar al terminar, y el PC no se
|
||||||
|
suspende mientras envía.
|
||||||
|
- Versión portable con Python incluido.
|
||||||
|
- Arreglado: solo con FTP (sin BFpilot), ftpsrv devolvía la raíz de la consola
|
||||||
|
al listar una carpeta; afectaba al explorador y al envío de carpetas.
|
||||||
|
- Abrir en el móvil: botón para dar permiso en el Firewall de Windows si la
|
||||||
|
página se queda en blanco.
|
||||||
|
|
||||||
|
NOVEDADES 1.19.2
|
||||||
|
- Arreglado "504 Option not supported" en Archivos de la consola cuando
|
||||||
|
BFpilot no está cargado: el explorador ya lista las carpetas solo con
|
||||||
|
ftpsrv (y con servidores FTP que no tienen MLSD).
|
||||||
|
|
||||||
|
NOVEDADES 1.19.1
|
||||||
|
- Arreglado: la cola dejaba de actualizarse sola (desde la 1.18.3), así que
|
||||||
|
"Quitar" parecía no hacer nada y luego decía "no existe".
|
||||||
|
- El explorador vuelve a listar la carpeta abierta cuando termina una subida
|
||||||
|
a esa carpeta.
|
||||||
|
- Arreglado "Descargar al PC" con aplicaciones: ftpsrv descifraba al vuelo los
|
||||||
|
eboot.bin/.prx/.sprx y la descarga salía incompleta. Ahora se transfieren
|
||||||
|
tal cual están (también mejora comparar tamaños al reanudar carpetas).
|
||||||
|
Gracias a RichyMX por encontrarlo y proponer el arreglo.
|
||||||
|
|
||||||
|
NOVEDADES 1.19
|
||||||
|
- Explorador de archivos renovado: menú de clic derecho, atajos de teclado,
|
||||||
|
ruta por tramos, unidades y accesos a la izquierda, "Mover a…" / "Copiar
|
||||||
|
a…" con destinos recientes, aviso al pegar si ya existe (reemplazar,
|
||||||
|
combinar, conservar ambos u omitir), "Extraer…" de comprimidos que ya
|
||||||
|
están en la consola, y arrastrar archivos del PC a la carpeta abierta.
|
||||||
|
- El explorador ya no necesita BFpilot para casi nada: renombrar, mover,
|
||||||
|
borrar, crear carpetas, permisos, tamaños y copiar funcionan solo con FTP.
|
||||||
|
- Campana de notificaciones arriba: transferencias terminadas o con error,
|
||||||
|
operaciones de archivos, desconexiones de la consola y versiones nuevas.
|
||||||
|
|
||||||
|
NOVEDADES 1.18.4
|
||||||
|
- Arreglado: si dejabas la ventana minimizada o en segundo plano un rato, el
|
||||||
|
programa se cerraba solo y al volver salía "Failed to fetch". Ahora solo se
|
||||||
|
cierra cuando cierras la ventana de verdad.
|
||||||
|
|
||||||
|
NOVEDADES 1.18.3
|
||||||
|
- Arreglado "Failed to fetch" y la cola que aparecía y desaparecía: en Windows
|
||||||
|
podían quedar dos PS5 Directo escuchando en el mismo puerto (por ejemplo al
|
||||||
|
abrirlo dos veces seguidas). Ahora solo puede haber uno abierto.
|
||||||
|
- Si la conexión con el programa falla un instante, se reintenta sola.
|
||||||
|
|
||||||
|
NOVEDADES 1.18.2
|
||||||
|
- Crea automáticamente un acceso directo "PS5 Directo" en el escritorio (y en "Enviar a")
|
||||||
|
con el icono del programa. Si lo borras no vuelve a aparecer; si apuntaba a
|
||||||
|
una copia vieja del programa, se corrige.
|
||||||
|
- La barra de tareas enseña el icono nuevo mientras el programa está abierto.
|
||||||
|
- Si se había quedado abierta una versión antigua (por ejemplo una copia vieja
|
||||||
|
en otra carpeta), al abrir esta se cierra la vieja sola en vez de enseñarla.
|
||||||
|
|
||||||
|
NOVEDADES 1.18.1
|
||||||
|
- Al borrar algo grande se ve cuánto se ha liberado y el tiempo que lleva, y el
|
||||||
|
espacio libre del explorador se actualiza mientras tanto.
|
||||||
|
|
||||||
NOVEDADES 1.18
|
NOVEDADES 1.18
|
||||||
- Pestaña "Espacio": gráfico de quesitos y lista de todo lo que ocupa sitio en
|
- Pestaña "Espacio": gráfico de quesitos y lista de todo lo que ocupa sitio en
|
||||||
la consola, con el nombre de cada juego y su tamaño. Marca juegos repetidos
|
la consola, con el nombre de cada aplicación y su tamaño. Marca contenido repetido
|
||||||
(por ejemplo la carpeta y su versión convertida), envíos a medias y restos,
|
(por ejemplo la carpeta y su versión convertida), envíos a medias y restos,
|
||||||
y se pueden borrar desde ahí. También con discos USB/externos.
|
y se pueden borrar desde ahí. También con discos USB/externos.
|
||||||
- Descargas más rápidas: cada conexión se reutiliza para muchos trozos
|
- Descargas más rápidas: cada conexión se reutiliza para muchos trozos
|
||||||
@@ -213,7 +618,7 @@ NOVEDADES 1.14.1
|
|||||||
|
|
||||||
NOVEDADES 1.14
|
NOVEDADES 1.14
|
||||||
- Guía de bienvenida la primera vez que se abre: enseña para qué es cada
|
- Guía de bienvenida la primera vez que se abre: enseña para qué es cada
|
||||||
parte. Se puede volver a ver con el botón "Guía" de arriba.
|
parte. Se puede volver a ver con el botón "Guía de uso" de arriba.
|
||||||
|
|
||||||
NOVEDADES 1.13.1
|
NOVEDADES 1.13.1
|
||||||
- El logo de la cabecera va dentro de la página (al actualizar desde una
|
- El logo de la cabecera va dentro de la página (al actualizar desde una
|
||||||
|
|||||||
@@ -1,21 +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 sh.Run("cmd /c where pyw >nul 2>nul", 0, True) = 0 Then
|
If fso.FileExists(dir & "\python\pythonw.exe") Then
|
||||||
py = "pyw -3"
|
py = """" & dir & "\python\pythonw.exe"""
|
||||||
ElseIf sh.Run("cmd /c where pythonw >nul 2>nul", 0, True) = 0 Then
|
ElseIf sh.Run("cmd /c where pyw >nul 2>nul", 0, True) = 0 Then
|
||||||
py = "pythonw"
|
py = "pyw -3"
|
||||||
End If
|
ElseIf sh.Run("cmd /c where pythonw >nul 2>nul", 0, True) = 0 Then
|
||||||
If py = "" Then
|
py = "pythonw"
|
||||||
MsgBox "No encuentro Python. Instala Python 3 (marca 'Add to PATH') o usa 'Iniciar PS5 Directo.bat'.", 48, "PS5 Directo"
|
End If
|
||||||
Else
|
If py = "" Then
|
||||||
sh.Run py & " """ & dir & "\app.py""" & args, 0, False
|
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"
|
||||||
End If
|
Else
|
||||||
|
sh.Run py & " """ & dir & "\app.py""" & args, 0, False
|
||||||
|
End If
|
||||||
@@ -2,27 +2,39 @@
|
|||||||
|
|
||||||
**English** · [Español](#español)
|
**English** · [Español](#español)
|
||||||
|
|
||||||
A Windows file-transfer tool for jailbroken PS5 consoles. It sends files to the console over FTP **while they download**, without storing them on your PC. It then extracts them, either on the console with BFpilot or on the PC with 7-Zip, and places apps in `/data/homebrew` so ShadowMountPlus shows them in the menu.
|
An all-in-one Windows tool for jailbroken PS5 consoles: it **downloads, converts and sends** in a single step. Sent as is, files go to the console over FTP **while they download**, without storing them on your PC; or it **converts them on the fly** (LZ4, `.exfat`, folder, `.ffpfsc`…) as part of the same job, no second program needed. It extracts on the console (BFpilot) or on the PC (7-Zip), places apps in `/data/homebrew` so ShadowMountPlus shows them in the menu, and lets you browse your console's library with covers and control everything from your phone.
|
||||||
|
|
||||||
- ✅ Streams downloads straight to the console: nothing is saved on the PC
|

|
||||||
- ✅ Works from download links, ftp:// links (e.g. a NAS) or files on your PC
|
|
||||||
- ✅ Joins split parts (.part1, .part2… / .001, .002…) and extracts RAR, 7z, ZIP and tar (.gz/.bz2/.xz)
|
<sub>Demo with a sample homebrew app served from a NAS on the local network.</sub>
|
||||||
- ✅ Extracts on the console (BFpilot) or on the PC (7-Zip, every file CRC-checked)
|
|
||||||
- ✅ Fixes the file permissions that stop BFpilot-extracted content from launching (CE-107750-0)
|
### ⚡ One step instead of three tools
|
||||||
- ✅ Pause, resume and retry where it left off, even after a dropped connection
|
- ✅ **Download and send at once:** files go straight to the console while they download, nothing stored on your PC
|
||||||
- ✅ Built-in console file browser: sizes, copy, move, rename, delete, download to PC, upload
|
- ✅ **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
|
||||||
- ✅ Storage view: a pie chart of what fills the console, by game name and size, flagging duplicates and leftovers you can delete
|
- ✅ **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")
|
||||||
- ✅ Send whole PC folders over several connections, skipping what's already there and resuming what got cut off
|
- ✅ **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
|
||||||
- ✅ Watches free space so the console never fills up
|
- ✅ **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)
|
||||||
- ✅ Speed limit and built-in speed test
|
|
||||||
- ✅ Download-manager style downloads: several reused connections per file, auto-backoff when a host complains
|
### 📚 Your console from your PC and phone
|
||||||
- ✅ Optional browser extension: links arrive by themselves with your browser session (same speed and access as the browser)
|
- ✅ **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
|
||||||
- ✅ Drag and drop, or "Send to" from Explorer
|
- ✅ **On your PC:** point it at your PC/NAS folders, see what's missing on the console and send it with one click
|
||||||
- ✅ Welcome guide, one-click updates, English and Spanish
|
- ✅ **Backup:** saved data, ShadowMountPlus/payload settings and the app database copied to the PC, manually or daily; restore saved data to the same console
|
||||||
|
- ✅ **Console files, Space and Captures:** file browser (copy, move, rename, upload, download), storage chart with "Make room", and the console's screenshots and videos
|
||||||
|
- ✅ **Phone and browser:** use it from your phone with a QR code, and send links from your browser with the optional extension
|
||||||
|
|
||||||
|
### 🩺 When something goes wrong
|
||||||
|
- ✅ **Diagnosis** on failed transfers: cause, error codes and how to fix it
|
||||||
|
- ✅ **Launch check:** if an app closes on launch, it checks the folder (executable, permissions, firmware, backports, LZ4 packing) and shows recent logs and captures
|
||||||
|
- ✅ **Problem report** in one click (.zip, no passwords) to share when asking for help
|
||||||
|
- ✅ **Warnings:** your PS5 firmware always visible, speed-drop alerts, "Why is it slow?" network check, and it loads ftpsrv/BFpilot from the app
|
||||||
|
- ✅ **Payloads on autopilot:** with Payload Manager open on the console, it starts ftpsrv and BFpilot when they're missing, restarts them if they hang, installs them if needed and can add them to Payload Manager's autoload
|
||||||
|
- ✅ **Self-updating**, with a welcome window and a user guide, in English and Spanish
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **PC:** Windows 10/11 and [Python 3.9+](https://www.python.org/downloads/) (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.
|
||||||
@@ -31,27 +43,60 @@ A Windows file-transfer tool for jailbroken PS5 consoles. It sends files to the
|
|||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Download the `.zip` 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**.
|
||||||
|
|
||||||
The full guide is in [README_EN.txt](README_EN.txt) (Spanish: [LEEME.txt](LEEME.txt)).
|
The full guide is in [README_EN.txt](README_EN.txt) (Spanish: [LEEME.txt](LEEME.txt)).
|
||||||
|
|
||||||
|
## 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 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
|
||||||
|
|
||||||
Independent project, not affiliated with Sony or with the authors of the tools mentioned. It's a file-transfer tool: it includes no third-party content, no Sony keys or files, and no links to download sites. Use it only with content you have the right to use (your own backups and homebrew).
|
Independent project, not affiliated with Sony or with the authors of the tools mentioned. It's a file-transfer tool: it includes no third-party content, no Sony keys or files, and no links to download sites. Use it only with content you have the right to use (your own backups and homebrew).
|
||||||
|
|
||||||
|
- It does not jailbreak the console or bypass any protection: it only copies files to a console you have already modified yourself.
|
||||||
|
- No piracy: don't use it to copy or run software you don't own or aren't licensed to use. Requests for help getting such content are not answered.
|
||||||
|
- You are responsible for how you use it. Modifying a console can void its warranty, break its terms of service or lose data: back up what matters.
|
||||||
|
- Provided "as is", without warranty of any kind. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment, used only to describe compatibility.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Español
|
## Español
|
||||||
|
|
||||||
Herramienta de transferencia de archivos para Windows: envía archivos a una PS5 con jailbreak por FTP **mientras se descargan**, sin pasar por el disco del PC. Después los descomprime (en la consola con BFpilot, o en el PC con 7-Zip) y coloca las aplicaciones en `/data/homebrew` para que ShadowMountPlus las muestre en el menú.
|

|
||||||
|
|
||||||
**Requisitos:** Windows 10/11, [Python 3.9+](https://www.python.org/downloads/) y, 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).
|
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.
|
||||||
|
|
||||||
**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).
|
**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.
|
||||||
|
|
||||||
**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).
|
**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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**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.
|
||||||
@@ -1,5 +1,31 @@
|
|||||||
PS5 DIRECTO 1.18 — USER GUIDE (English)
|
PS5 DIRECTO 1.20.0 — USER GUIDE (English)
|
||||||
=======================================
|
=========================================
|
||||||
|
|
||||||
|
QUICK START (5 MINUTES)
|
||||||
|
=======================
|
||||||
|
You need: a PS5 that is already jailbroken (PS5 Directo doesn't do that)
|
||||||
|
and a Windows 10/11 PC on the same network.
|
||||||
|
|
||||||
|
1. Download PS5Directo-X.Y.Z-Windows.zip from the GitHub Releases
|
||||||
|
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
|
||||||
|
downloaded from the internet, click "Open".
|
||||||
|
3. On the PS5, load your jailbreak and ShadowMountPlus as usual.
|
||||||
|
4. The wizard asks for the PS5's IP: on the PS5, Settings > Network >
|
||||||
|
View Connection Status > "IPv4 Address".
|
||||||
|
5. It checks the FTP server and BFpilot. If one is red, click "Load" next to
|
||||||
|
it. (With Y2JB Autoloader that doesn't work: load them from Payload
|
||||||
|
Manager on the PS5 and click "Check again".)
|
||||||
|
6. Paste a direct download link (or drag a file onto the window) and click
|
||||||
|
"Add to queue". When it says "Done", it's in the PS5's menu.
|
||||||
|
|
||||||
|
Something wrong? Settings > "Full check" tells you what and how to fix it.
|
||||||
|
|
||||||
|
Note: PS5 Directo only copies files. Use it with content you have the
|
||||||
|
right to use (your own backups and homebrew).
|
||||||
|
|
||||||
|
(The full guide follows; you don't need it to get started.)
|
||||||
|
|
||||||
|
|
||||||
Paste links (or drag files from your PC) and the tool does the rest:
|
Paste links (or drag files from your PC) and the tool does the rest:
|
||||||
|
|
||||||
@@ -23,13 +49,20 @@ TABS
|
|||||||
destination folder), delete, download to the PC (to Downloads\PS5
|
destination folder), delete, download to the PC (to Downloads\PS5
|
||||||
Directo), set 777 permissions, and upload files or folders as-is into
|
Directo), set 777 permissions, and upload files or folders as-is into
|
||||||
the current folder.
|
the current folder.
|
||||||
Copy, move, delete and folder sizes need BFpilot; without it the
|
Right-click a file or the empty space for the menu; shortcuts: Del,
|
||||||
browser is read-only over FTP.
|
F2, Ctrl+C / Ctrl+X / Ctrl+V, Ctrl+A and Backspace. "Move to…" and
|
||||||
|
"Copy to…" open a folder picker with recent destinations. If something
|
||||||
|
with the same name already exists when pasting, you choose replace/merge,
|
||||||
|
keep both or skip. "Extract…" unpacks a .rar/.7z/.zip that is already on
|
||||||
|
the console. Drives (with free space) and quick places are on the left.
|
||||||
|
You can drag files from the PC onto the open folder.
|
||||||
|
Without BFpilot almost everything works over FTP (copying goes through
|
||||||
|
the PC, slower); extracting and showing free space do need BFpilot.
|
||||||
- Space: a pie chart of what takes up space on the console, with each
|
- Space: a pie chart of what takes up space on the console, with each
|
||||||
game's name and size. It flags repeated games (for example a folder and
|
app's name and size. It flags duplicate content (for example a folder and
|
||||||
its version converted to an image), half-sent transfers and leftovers,
|
its version converted to an image), half-sent transfers and leftovers,
|
||||||
and lets you delete them from there (it asks first). Needs BFpilot.
|
and lets you delete them from there (it asks first). Needs BFpilot.
|
||||||
Games installed from the PS5 itself are shown but deleted on the console.
|
Anything installed from the PS5 itself is shown but deleted on the console.
|
||||||
- Activity: history of finished and failed transfers.
|
- Activity: history of finished and failed transfers.
|
||||||
|
|
||||||
PC FOLDERS
|
PC FOLDERS
|
||||||
@@ -63,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:
|
||||||
@@ -97,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.
|
||||||
|
|
||||||
@@ -119,7 +157,7 @@ HOW TO USE IT
|
|||||||
-------------
|
-------------
|
||||||
1. Extract this folder anywhere you like.
|
1. Extract this folder anywhere you like.
|
||||||
2. Open PS5 Directo from the desktop icon. A short welcome guide explains
|
2. Open PS5 Directo from the desktop icon. A short welcome guide explains
|
||||||
each part (you can see it again with the "Guide" button).
|
each part (you can see it again with the "User guide" button).
|
||||||
3. The first time, Settings opens: enter your PS5's IP (on the PS5:
|
3. The first time, Settings opens: enter your PS5's IP (on the PS5:
|
||||||
Settings > Network > View Connection Status), click "Save" and then
|
Settings > Network > View Connection Status), click "Save" and then
|
||||||
"Test connection". Both dots at the top should turn green.
|
"Test connection". Both dots at the top should turn green.
|
||||||
@@ -218,10 +256,306 @@ Images (.exfat, .ffpkg, .ffpfsc) go to /data/homebrew.
|
|||||||
Settings and queue are stored in %USERPROFILE%\.ps5directo
|
Settings and queue are stored in %USERPROFILE%\.ps5directo
|
||||||
|
|
||||||
|
|
||||||
|
LIBRARY
|
||||||
|
-------
|
||||||
|
The Library tab shows what's in /data/homebrew (and on USB drives) with its
|
||||||
|
cover, name, Title ID, version, folder or image (.ffpkg, .ffpfsc, .exfat)
|
||||||
|
and the firmware it needs. Set your PS5's firmware in Settings (e.g. 13.60)
|
||||||
|
and the ones that won't launch are flagged in red. It also flags duplicates,
|
||||||
|
DLC/updates and incomplete folders. Works with FTP alone; with BFpilot it
|
||||||
|
also shows how much each folder takes. Firmware comes from param.json and,
|
||||||
|
for backported titles (fakelib folder), from what the executables require.
|
||||||
|
Every finished transfer is checked too.
|
||||||
|
|
||||||
|
IMAGES AND LZ4 (PS5 DUMP FORGE)
|
||||||
|
-------------------------------
|
||||||
|
"How to leave it on the PS5" defaults to "No conversion": left as it comes
|
||||||
|
(archives are extracted; an .ffpkg, .exfat or .ffpfsc image or LZ4 content
|
||||||
|
is sent untouched). You can also pick:
|
||||||
|
- Plain folder: always ends up as a folder; images, .fpkg and LZ4 are
|
||||||
|
unpacked on the PC.
|
||||||
|
- .ffpkg image: recommended by ShadowMountPlus. Full speed.
|
||||||
|
- .exfat or .ffpfs image: ShadowMountPlus mounts them too.
|
||||||
|
- Compressed .ffpfsc image: 30-60% smaller, but read more slowly
|
||||||
|
(150-250 MB/s); some titles may stutter.
|
||||||
|
- .fpkg package: installs like a store app. Firmware 11.60 or lower only
|
||||||
|
(kstuff, fpkg-enable and ppr-patch). Left in /data/pkg. For .fpkg and
|
||||||
|
.ffpfsc you can pick the compression.
|
||||||
|
- LZ4 (folder or inside an .ffpkg): AMPR titles only. Experimental: some
|
||||||
|
titles crash when saving. Non-AMPR content is sent as a folder or plain
|
||||||
|
.ffpkg.
|
||||||
|
It's prepared on the PC with PS5 Dump Forge (github.com/quer3q/ps5-dump-forge,
|
||||||
|
downloaded automatically the first time) and sent as a single file. Needs
|
||||||
|
room on the PC for the image. .ffpkg/.ffpfsc images are not FPKG packages:
|
||||||
|
they aren't installed, ShadowMountPlus mounts them. DLC and updates are
|
||||||
|
sent as folders.
|
||||||
|
|
||||||
|
BEFORE SENDING
|
||||||
|
--------------
|
||||||
|
- If it's already on the console (same Title ID), the transfer
|
||||||
|
stops and asks: send anyway, replace the copy there, or cancel.
|
||||||
|
- Paste the .sfv / .md5 / .sha1 with the parts (or keep it next to them on
|
||||||
|
the PC) and every part is checked while it's sent.
|
||||||
|
- When there's no room, the error has a "Make room" button that suggests
|
||||||
|
what to delete in the Space tab. Nothing is deleted without confirmation.
|
||||||
|
|
||||||
|
PAYLOADS, DIAGNOSTICS, PHONE AND SCHEDULE
|
||||||
|
-----------------------------------------
|
||||||
|
- Click the FTP / BFpilot dots (or Settings > Load payloads) to send ftpsrv
|
||||||
|
or BFpilot to the PS5's ELF loader (port 9021, etaHEN or elfldr). The Y2JB
|
||||||
|
Autoloader loader only accepts the console itself: use Payload Manager.
|
||||||
|
- "Why is it slow?" checks Wi-Fi vs cable, band and signal, 100 Mb/s links,
|
||||||
|
VPNs, different networks and the ping to the PS5.
|
||||||
|
- The phone button shows a QR code to open PS5 Directo from your phone on
|
||||||
|
the same network.
|
||||||
|
- "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.
|
||||||
|
|
||||||
|
WHAT TO DOWNLOAD
|
||||||
|
----------------
|
||||||
|
- PS5Directo-X.Y.Z-Setup.exe: the installer. This is the one to get.
|
||||||
|
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.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,
|
||||||
|
firmware, backports and LZ4 packing), shows logs (.log) and captures
|
||||||
|
from the last minutes, explains the error code shown on the TV and
|
||||||
|
saves a .zip report. Read-only: it changes nothing.
|
||||||
|
- Welcome window on first launch: sums up what PS5 Directo is and does.
|
||||||
|
- Library: also what's installed on the console (PS4 and PS5), with
|
||||||
|
cover, version and firmware, and a filter for it.
|
||||||
|
- Library, "On your PC": pick PC or NAS folders, see what's already on
|
||||||
|
the console and what isn't, and send it with one click.
|
||||||
|
- Library: "See updates" opens each title's public official update list
|
||||||
|
in your browser (Prospero Patches). Warns that updates needing
|
||||||
|
firmware 14.00 or higher can't be installed. The program downloads
|
||||||
|
nothing.
|
||||||
|
- Library: if PS5 Game Updater is on the console, a button opens it; if
|
||||||
|
its folder is misplaced, it detects and fixes it.
|
||||||
|
- Library: images and folders without param.json show name, cover and
|
||||||
|
firmware.
|
||||||
|
- New "Backup" tab: saves saved data, ShadowMountPlus and payload
|
||||||
|
settings and the app database to the PC, manually or daily. Saved data
|
||||||
|
can be restored to the same console (current data is backed up first).
|
||||||
|
- New "Captures" tab: the console's screenshots and videos with
|
||||||
|
thumbnails, to view and download to the PC.
|
||||||
|
- "Diagnosis" on failed transfers: what happened, codes and how to fix
|
||||||
|
it, plus a .zip report (no passwords) to send to whoever helps you. In
|
||||||
|
Settings, "Create problem report".
|
||||||
|
- Torrents: paste a magnet or a .torrent. It's downloaded to the PC
|
||||||
|
first with aria2 (fetched automatically) and then sent, extracted or
|
||||||
|
converted. No seeding afterwards; deleted from the PC.
|
||||||
|
- PS5 Dump Forge updates itself (at most daily), checking first the new
|
||||||
|
version works; otherwise it keeps the previous one.
|
||||||
|
- New PS5 Directo versions are announced by themselves in a window with
|
||||||
|
what's new.
|
||||||
|
- Warns if the speed to the PS5 suddenly drops (usually the console
|
||||||
|
idling: press a controller button).
|
||||||
|
- "Format on the PS5" starts folded and says how long it takes: no
|
||||||
|
conversion downloads and sends at once; converting takes 3 steps.
|
||||||
|
Cards sorted by most used and "Which one?" by need.
|
||||||
|
- Your PS5 firmware next to the title (saves automatically) and the
|
||||||
|
.fpkg card tells you if it works.
|
||||||
|
- In the queue, "Convert to …" names the format, and it warns if
|
||||||
|
something couldn't be converted.
|
||||||
|
- Fixed: an archive with a normal version plus a backport wasn't
|
||||||
|
converted; now it picks the right one.
|
||||||
|
- Fixed: converted files got mixed into an older folder with the same
|
||||||
|
name.
|
||||||
|
- Fixed: many Library folders showed no name.
|
||||||
|
1.20.1 - If it can't connect and the PC and PS5 are on different networks,
|
||||||
|
it says so and explains how to fix it.
|
||||||
|
- Clearer "Use on your phone" and "Browser extension" buttons, also
|
||||||
|
offered at the end of the first-steps wizard.
|
||||||
|
- "Download again" in the queue and in Activity: starts over without
|
||||||
|
pasting the link again. If the target already exists when placing,
|
||||||
|
it asks (keep both, replace or cancel) instead of failing.
|
||||||
|
- Fixed: archives with the normal app plus a "backport" folder no
|
||||||
|
longer fail with "Already exists"; half-sent leftovers are
|
||||||
|
replaced.
|
||||||
|
- Updated "User guide": explains the format converter, every tab,
|
||||||
|
phone access and the browser extension.
|
||||||
|
- The "Which one?" window adds estimated speeds where there was no
|
||||||
|
data, with clearer notes on LZ4 and .ffpfsc.
|
||||||
|
1.20.0 - First-steps wizard (IP, connection check and loading what's
|
||||||
|
missing, firmware) and a shorter, simpler guide.
|
||||||
|
- New Library tab: what's on the console with cover, version, format
|
||||||
|
and required firmware, flagging what won't launch on yours.
|
||||||
|
- .ffpkg / .ffpfsc images and LZ4 with PS5 Dump Forge (optional):
|
||||||
|
prepared on the PC and sent as a single file. Picked with cards
|
||||||
|
under the destination, each explained, with the room needed on the
|
||||||
|
PC; the first time it warns that converting takes PC space until
|
||||||
|
it's done. A download that's already an image is sent as is or
|
||||||
|
converted to the chosen format, and placed even without BFpilot.
|
||||||
|
- New formats: plain folder, .exfat, .ffpfs and .fpkg (with a
|
||||||
|
compression choice). Without 7-Zip, PS5 Dump Forge extracts it.
|
||||||
|
- Formats are grouped (space savers first) and "Which one?" compares
|
||||||
|
the pros and cons of each.
|
||||||
|
- Time left (and rough finish time) when sending, extracting and
|
||||||
|
converting. On the phone, a big alert with vibration and a beep
|
||||||
|
when something finishes or fails (with the page open).
|
||||||
|
- Library: LZ4-packed folders are tagged LZ4 (plus an LZ4 filter).
|
||||||
|
- Fixed: resuming a transfer asked "Already on the console" about
|
||||||
|
its own half-sent copy.
|
||||||
|
- Fixed: the phone page was blank in some Android Chrome versions.
|
||||||
|
- Open on phone: the page is sent compressed and the dialog shows
|
||||||
|
"What arrives from the phone" to tell whether it gets there.
|
||||||
|
- Firmware warning (set yours in Settings), backport-aware.
|
||||||
|
- Asks before sending something that's already on the console.
|
||||||
|
- .sfv / .md5 / .sha1 checks while sending.
|
||||||
|
- "Make room": suggests what to delete when space runs out.
|
||||||
|
- Load ftpsrv and BFpilot on the PS5 from the app (port 9021).
|
||||||
|
- "Why is it slow?" network diagnostics.
|
||||||
|
- Open from your phone with a QR code.
|
||||||
|
- Schedule the queue, sleep or shut down when done, no sleep while
|
||||||
|
sending.
|
||||||
|
- Portable version with Python included.
|
||||||
|
- Fixed: with FTP only (no BFpilot), ftpsrv returned the console's
|
||||||
|
root when listing a folder; it affected the file browser and
|
||||||
|
folder uploads.
|
||||||
|
- Phone access: a button to allow it in Windows Firewall if the page
|
||||||
|
stays blank.
|
||||||
|
1.19.2 - Fixed "504 Option not supported" in Console files when BFpilot is
|
||||||
|
not loaded: the file browser now lists folders with ftpsrv alone
|
||||||
|
(and with FTP servers that have no MLSD).
|
||||||
|
1.19.1 - Fixed: the queue stopped updating by itself (since 1.18.3), so
|
||||||
|
"Remove" seemed to do nothing and then said "no existe". The file
|
||||||
|
browser now relists the open folder when an upload to it finishes.
|
||||||
|
Fixed "Download to PC" for apps: ftpsrv decrypted eboot.bin/.prx/
|
||||||
|
.sprx on the fly, so downloads came out incomplete. Files are now
|
||||||
|
transferred exactly as stored (this also makes size comparisons
|
||||||
|
when resuming folders correct). Thanks to RichyMX for finding it.
|
||||||
|
1.19 - Renewed file browser: right-click menu, keyboard shortcuts,
|
||||||
|
clickable path, drives and places on the left, "Move to…" / "Copy
|
||||||
|
to…" with recent destinations, a choice when pasting over existing
|
||||||
|
items (replace, merge, keep both or skip), "Extract…" for archives
|
||||||
|
already on the console, and dragging files from the PC onto the
|
||||||
|
open folder.
|
||||||
|
- The file browser no longer needs BFpilot for most things: rename,
|
||||||
|
move, delete, new folder, permissions, sizes and copy work over FTP.
|
||||||
|
- Notification bell at the top: finished or failed transfers, file
|
||||||
|
operations, console disconnections and new versions.
|
||||||
|
1.18.4 - Fixed: leaving the window minimized or in the background for a while
|
||||||
|
made the program close itself, showing "Failed to fetch" on return.
|
||||||
|
Now it only closes when you actually close the window.
|
||||||
|
1.18.3 - Fixed "Failed to fetch" and the queue appearing and disappearing: on
|
||||||
|
Windows two PS5 Directo could end up listening on the same port (e.g.
|
||||||
|
when opened twice in a row). Now only one can be open. Brief
|
||||||
|
connection hiccups are retried automatically.
|
||||||
|
1.18.2 - Creates a "PS5 Directo" shortcut on the desktop (and in "Send to")
|
||||||
|
with the program's icon. If you delete it, it won't come back; if it
|
||||||
|
pointed to an old copy, it's fixed. The taskbar shows the new icon.
|
||||||
|
If an old version was still open (e.g. an old copy in another folder),
|
||||||
|
it's closed automatically instead of being shown.
|
||||||
|
1.18.1 - When deleting something big you see how much has been freed and how
|
||||||
|
long it has taken, and the browser's free space updates meanwhile.
|
||||||
1.18 - "Space" tab: a pie chart and list of everything that takes up space
|
1.18 - "Space" tab: a pie chart and list of everything that takes up space
|
||||||
on the console, by game name and size. It flags repeated games (for
|
on the console, by app name and size. It flags duplicate content (for
|
||||||
example a folder and its converted version), half-sent transfers and
|
example a folder and its converted version), half-sent transfers and
|
||||||
leftovers, and deletes them from there. USB/external drives too.
|
leftovers, and deletes them from there. USB/external drives too.
|
||||||
- Faster downloads: each connection is reused for many chunks in a row,
|
- Faster downloads: each connection is reused for many chunks in a row,
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
const EN = !(navigator.language || "es").toLowerCase().startsWith("es");
|
|
||||||
// PS5 Directo - captura de enlaces.
|
|
||||||
// Cuando el navegador empieza a descargar un archivo comprimido o paquete (.rar, .7z, .zip, .pkg...),
|
|
||||||
// manda el enlace FINAL (despues de redirecciones) a PS5 Directo y cancela la descarga.
|
|
||||||
// Si PS5 Directo no esta abierto, no toca nada: la descarga sigue normal.
|
|
||||||
|
|
||||||
const WANTED_FILE = /\.(part\d+\.rar|rar|r\d{2,3}|7z(\.\d{3})?|zip|z\d{2}|\d{3}|pkg|exfat|ffpkg|ffpfsc?|ufs)$/i;
|
|
||||||
const PORTS = [8777, 8778, 8779];
|
|
||||||
|
|
||||||
async function settings() {
|
|
||||||
return chrome.storage.local.get({ enabled: true, captured: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findApp() {
|
|
||||||
for (const port of PORTS) {
|
|
||||||
try {
|
|
||||||
const r = await fetch(`http://127.0.0.1:${port}/api/ext/ping`, { cache: "no-store" });
|
|
||||||
if (r.ok) {
|
|
||||||
const j = await r.json();
|
|
||||||
if (j.app === "PS5 Directo") return { port, version: j.version };
|
|
||||||
}
|
|
||||||
} catch (e) { /* no esta en ese puerto */ }
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileName(item) {
|
|
||||||
const f = (item.filename || "").split(/[\\/]/).pop();
|
|
||||||
if (f) return f;
|
|
||||||
try { return decodeURIComponent(new URL(item.finalUrl || item.url).pathname.split("/").pop()); }
|
|
||||||
catch (e) { return ""; }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handle(item) {
|
|
||||||
const cfg = await settings();
|
|
||||||
if (!cfg.enabled) return;
|
|
||||||
const url = item.finalUrl || item.url;
|
|
||||||
if (!/^https?:/i.test(url)) return;
|
|
||||||
const name = fileName(item);
|
|
||||||
const path = url.split("?")[0];
|
|
||||||
if (!WANTED_FILE.test(name) && !WANTED_FILE.test(path)) return; // no es un archivo que nos interese
|
|
||||||
const app = await findApp();
|
|
||||||
if (!app) { // PS5 Directo cerrado: no tocar
|
|
||||||
chrome.action.setBadgeText({ text: "off" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const r = await fetch(`http://127.0.0.1:${app.port}/api/ext/capture`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ url, original: item.url, name })
|
|
||||||
});
|
|
||||||
if (!r.ok) return;
|
|
||||||
} catch (e) { return; }
|
|
||||||
try { await chrome.downloads.cancel(item.id); } catch (e) {}
|
|
||||||
try { await chrome.downloads.erase({ id: item.id }); } catch (e) {}
|
|
||||||
const n = (cfg.captured || 0) + 1;
|
|
||||||
await chrome.storage.local.set({ captured: n, last: name });
|
|
||||||
chrome.action.setBadgeBackgroundColor({ color: "#2563eb" });
|
|
||||||
chrome.action.setBadgeText({ text: String(n % 1000) });
|
|
||||||
chrome.notifications.create({
|
|
||||||
type: "basic", iconUrl: "icon128.png", title: EN ? "Sent to PS5 Directo" : "Enviado a PS5 Directo",
|
|
||||||
message: (name || (EN ? "Link" : "Enlace")) + (EN ? " — not downloaded to the PC." : " — no se descarga en el PC.")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cuando ya se sabe el nombre del archivo y el enlace final (tras las redirecciones)
|
|
||||||
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
||||||
suggest(); // no cambiamos el nombre
|
|
||||||
handle(item);
|
|
||||||
});
|
|
||||||
|
After Width: | Height: | Size: 896 KiB |
|
After Width: | Height: | Size: 928 KiB |
@@ -762,6 +762,110 @@ def make_source(text):
|
|||||||
return FileSource(text)
|
return FileSource(text)
|
||||||
|
|
||||||
|
|
||||||
|
_MONTHS = {m: i for i, m in enumerate(("jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep",
|
||||||
|
"oct", "nov", "dec"), 1)}
|
||||||
|
|
||||||
|
|
||||||
|
def _list_mode(perm):
|
||||||
|
"""'drwxr-xr-x' -> 0o755 (None si no se entiende)."""
|
||||||
|
if len(perm) < 10:
|
||||||
|
return None
|
||||||
|
m = 0
|
||||||
|
for i, ch in enumerate(perm[1:10]):
|
||||||
|
if ch not in "-":
|
||||||
|
m |= 1 << (8 - i)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def _list_mtime(mon, day, yt):
|
||||||
|
import time as _t
|
||||||
|
try:
|
||||||
|
mo = _MONTHS[mon[:3].lower()]
|
||||||
|
d = int(day)
|
||||||
|
if ":" in yt:
|
||||||
|
hh, mm = (int(x) for x in yt.split(":"))
|
||||||
|
now = _t.localtime()
|
||||||
|
y = now.tm_year if (mo, d) <= (now.tm_mon, now.tm_mday) else now.tm_year - 1
|
||||||
|
else:
|
||||||
|
y, hh, mm = int(yt), 0, 0
|
||||||
|
return int(_t.mktime((y, mo, d, hh, mm, 0, 0, 0, -1)))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ftp_listdir(f, path):
|
||||||
|
"""[(nombre, {"type": "file"|"dir", "size": int, "mtime": int|None, "mode": int|None})].
|
||||||
|
Usa MLSD tal cual (sin OPTS MLST: ftpsrv responde "504 Option not supported") y, si el
|
||||||
|
servidor no tiene MLSD, LIST. Lanza ftplib.error_perm 550 si la carpeta no existe."""
|
||||||
|
import time as _t
|
||||||
|
out = []
|
||||||
|
# Primero se entra en la carpeta y se lista "lo de aqui": ftpsrv no hace caso a la ruta de
|
||||||
|
# MLSD/LIST y devolvia la carpeta actual (la raiz de la PS5). Si no existe, CWD da 550.
|
||||||
|
f.cwd(path)
|
||||||
|
try:
|
||||||
|
lines = []
|
||||||
|
f.retrlines("MLSD", lines.append)
|
||||||
|
for ln in lines:
|
||||||
|
facts_s, _, name = ln.partition(" ")
|
||||||
|
if not name or name in (".", ".."):
|
||||||
|
continue
|
||||||
|
facts = {}
|
||||||
|
for kv in facts_s.split(";"):
|
||||||
|
k, _, v = kv.partition("=")
|
||||||
|
if k:
|
||||||
|
facts[k.lower()] = v
|
||||||
|
t = facts.get("type", "").lower()
|
||||||
|
if t not in ("file", "dir"):
|
||||||
|
continue
|
||||||
|
mt = facts.get("modify")
|
||||||
|
try:
|
||||||
|
mt = int(_t.mktime(_t.strptime(mt[:14], "%Y%m%d%H%M%S"))) if mt else None
|
||||||
|
except Exception:
|
||||||
|
mt = None
|
||||||
|
mode = facts.get("unix.mode")
|
||||||
|
try:
|
||||||
|
mode = int(mode, 8) if mode else None
|
||||||
|
except ValueError:
|
||||||
|
mode = None
|
||||||
|
try:
|
||||||
|
size = int(facts.get("size") or 0)
|
||||||
|
except ValueError:
|
||||||
|
size = 0
|
||||||
|
out.append((name, {"type": t, "size": size, "mtime": mt, "mode": mode}))
|
||||||
|
return out
|
||||||
|
except ftplib.error_perm as e:
|
||||||
|
if str(e).startswith("550"):
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
f.voidcmd("TYPE I")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# servidor sin MLSD: LIST al estilo "ls -l"
|
||||||
|
lines = []
|
||||||
|
try:
|
||||||
|
f.retrlines("LIST", lines.append)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
f.voidcmd("TYPE I")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for ln in lines:
|
||||||
|
parts = ln.split(None, 8)
|
||||||
|
if len(parts) < 9 or parts[8] in (".", ".."):
|
||||||
|
continue
|
||||||
|
kind = "dir" if ln.startswith("d") else ("file" if ln.startswith("-") else None)
|
||||||
|
if not kind:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
size = int(parts[4])
|
||||||
|
except ValueError:
|
||||||
|
size = 0
|
||||||
|
out.append((parts[8], {"type": kind, "size": size, "mtime": _list_mtime(parts[5], parts[6], parts[7]),
|
||||||
|
"mode": _list_mode(parts[0])}))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def ftp_sizes(f, path):
|
def ftp_sizes(f, path):
|
||||||
"""{nombre: tamano} de los archivos de una carpeta remota (None si no existe)."""
|
"""{nombre: tamano} de los archivos de una carpeta remota (None si no existe)."""
|
||||||
try:
|
try:
|
||||||
@@ -774,31 +878,11 @@ def ftp_sizes(f, path):
|
|||||||
|
|
||||||
|
|
||||||
def _ftp_sizes(f, path):
|
def _ftp_sizes(f, path):
|
||||||
out = {}
|
|
||||||
try:
|
try:
|
||||||
for name, facts in f.mlsd(path, facts=["type", "size"]):
|
ents = ftp_listdir(f, path)
|
||||||
if facts.get("type") == "file":
|
|
||||||
out[name] = int(facts.get("size") or 0)
|
|
||||||
return out
|
|
||||||
except ftplib.error_perm as e:
|
|
||||||
if str(e).startswith("550"):
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# servidor sin MLSD: LIST y leer el tamano de cada linea
|
|
||||||
lines = []
|
|
||||||
try:
|
|
||||||
f.retrlines("LIST " + path, lines.append)
|
|
||||||
except ftplib.error_perm:
|
except ftplib.error_perm:
|
||||||
return None
|
return None
|
||||||
for ln in lines:
|
return {n: x["size"] for n, x in ents if x["type"] == "file"}
|
||||||
parts = ln.split(None, 8)
|
|
||||||
if len(parts) == 9 and not ln.startswith("d"):
|
|
||||||
try:
|
|
||||||
out[parts[8]] = int(parts[4])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def upload_tree(cfg, src, remote_root, cancel, log, progress, workers=None, on_plan=None):
|
def upload_tree(cfg, src, remote_root, cancel, log, progress, workers=None, on_plan=None):
|
||||||
@@ -967,6 +1051,21 @@ def clean_host(text):
|
|||||||
return t.strip(".").strip("[]"), port
|
return t.strip(".").strip("[]"), port
|
||||||
|
|
||||||
|
|
||||||
|
def raw_mode(f):
|
||||||
|
"""ftpsrv (ps5-payload-dev) descifra al vuelo los SELF (eboot.bin, .prx, .sprx): al bajarlos
|
||||||
|
llega el ELF de dentro, con otro tamano, y los listados tambien dan ese tamano. Su comando
|
||||||
|
SELF lo activa/desactiva en cada conexion: lo dejamos apagado para ver los archivos tal
|
||||||
|
cual estan. Los servidores que no lo conocen responden con error y no pasa nada.
|
||||||
|
(Gracias a RichyMX por encontrarlo.)"""
|
||||||
|
try:
|
||||||
|
r = f.sendcmd("SELF")
|
||||||
|
if "enabled" in r.lower():
|
||||||
|
r = f.sendcmd("SELF")
|
||||||
|
return "disabled" in r.lower()
|
||||||
|
except ftplib.all_errors:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Ftp:
|
class Ftp:
|
||||||
def __init__(self, cfg):
|
def __init__(self, cfg):
|
||||||
self.host = clean_host(cfg["ps5_ip"])[0]
|
self.host = clean_host(cfg["ps5_ip"])[0]
|
||||||
@@ -987,6 +1086,7 @@ class Ftp:
|
|||||||
f.login(self.user, self.pw)
|
f.login(self.user, self.pw)
|
||||||
f.set_pasv(True)
|
f.set_pasv(True)
|
||||||
f.voidcmd("TYPE I")
|
f.voidcmd("TYPE I")
|
||||||
|
raw_mode(f)
|
||||||
try:
|
try:
|
||||||
f.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
f.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1257,11 +1357,16 @@ class BFpilot:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def wait_job(self, cancel, timeout=3600):
|
def wait_job(self, cancel, timeout=3600, on_tick=None):
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
while time.time() - t0 < timeout:
|
while time.time() - t0 < timeout:
|
||||||
if cancel is not None and cancel.is_set():
|
if cancel is not None and cancel.is_set():
|
||||||
raise Cancelled()
|
raise Cancelled()
|
||||||
|
if on_tick is not None:
|
||||||
|
try:
|
||||||
|
on_tick()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
j = self._req("GET", "/api/fs/job/status")
|
j = self._req("GET", "/api/fs/job/status")
|
||||||
if not j.get("busy"):
|
if not j.get("busy"):
|
||||||
if j.get("error"):
|
if j.get("error"):
|
||||||
@@ -1270,7 +1375,7 @@ class BFpilot:
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
raise PipeError("BFpilot tarda demasiado")
|
raise PipeError("BFpilot tarda demasiado")
|
||||||
|
|
||||||
def delete(self, path, cancel=None):
|
def delete(self, path, cancel=None, on_tick=None):
|
||||||
for _ in range(60):
|
for _ in range(60):
|
||||||
try:
|
try:
|
||||||
self._req("POST", "/api/fs/delete", form={"path": path, "recursive": "1"})
|
self._req("POST", "/api/fs/delete", form={"path": path, "recursive": "1"})
|
||||||
@@ -1282,7 +1387,7 @@ class BFpilot:
|
|||||||
if "No such file" in str(e) or "not found" in str(e).lower():
|
if "No such file" in str(e) or "not found" in str(e).lower():
|
||||||
return
|
return
|
||||||
raise
|
raise
|
||||||
self.wait_job(cancel)
|
self.wait_job(cancel, timeout=4 * 3600, on_tick=on_tick)
|
||||||
|
|
||||||
def du(self, path, timeout=20):
|
def du(self, path, timeout=20):
|
||||||
"""Tamano total de una carpeta (o archivo), o None."""
|
"""Tamano total de una carpeta (o archivo), o None."""
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 778 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,981 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>PS5 Directo</title>
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<style>
|
|
||||||
:root{
|
|
||||||
--bg:#0e1116;--panel:#161b22;--panel2:#1c232c;--line:#2a323d;--text:#e6edf3;--muted:#8b97a5;
|
|
||||||
--accent:#3d8bfd;--accent2:#2f6fd0;--ok:#2ea043;--warn:#d29922;--err:#e5534b;--radius:12px;
|
|
||||||
}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
[hidden]{display:none!important}
|
|
||||||
body{margin:0;color:var(--text);font:15px/1.45 "Segoe UI",system-ui,-apple-system,sans-serif;min-height:100vh;background-color:var(--bg);background-image:radial-gradient(900px 600px at 0% 0%,rgba(58,123,255,.13),transparent 60%),radial-gradient(800px 600px at 100% 100%,rgba(123,63,228,.12),transparent 60%),url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22260%22 height=%22260%22 viewBox=%220 0 260 260%22%3E%3Cg fill=%22none%22 stroke=%22%23ffffff%22 stroke-opacity=%22.075%22 stroke-width=%222.4%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath transform=%22translate%2840 48%29 rotate%28-12%29%22 d=%22M0 -15.0 L13.0 7.5 L-13.0 7.5 Z%22/%3E%3Ccircle cx=%22150%22 cy=%2236%22 r=%2212.0%22/%3E%3Cpath transform=%22translate%28228 92%29 rotate%288%29%22 d=%22M-12.0 -12.0 L12.0 12.0 M12.0 -12.0 L-12.0 12.0%22/%3E%3Crect transform=%22translate%2896 128%29 rotate%2814%29%22 x=%22-11.0%22 y=%22-11.0%22 width=%2222%22 height=%2222%22 rx=%223%22/%3E%3Ccircle cx=%2230%22 cy=%22190%22 r=%2210.0%22/%3E%3Cpath transform=%22translate%28196 200%29 rotate%2818%29%22 d=%22M0 -12.7 L11.0 6.4 L-11.0 6.4 Z%22/%3E%3Crect transform=%22translate%28232 232%29 rotate%28-10%29%22 x=%22-9.0%22 y=%22-9.0%22 width=%2218%22 height=%2218%22 rx=%223%22/%3E%3Cpath transform=%22translate%28124 232%29 rotate%28-6%29%22 d=%22M-10.0 -10.0 L10.0 10.0 M10.0 -10.0 L-10.0 10.0%22/%3E%3Ccircle cx=%22186%22 cy=%22140%22 r=%228.0%22/%3E%3Cpath transform=%22translate%2858 194%29 rotate%2830%29%22 d=%22M0 -10.4 L9.0 5.2 L-9.0 5.2 Z%22/%3E%3C/g%3E%3C/svg%3E");background-attachment:fixed;background-size:auto,auto,260px 260px}
|
|
||||||
header{display:flex;align-items:center;gap:14px;padding:16px 24px;border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(14,17,22,.82);backdrop-filter:blur(6px);z-index:5}
|
|
||||||
header h1{font-size:19px;margin:0;font-weight:650;letter-spacing:.2px}
|
|
||||||
header .sub{color:var(--muted);font-size:13px}
|
|
||||||
.spacer{flex:1}
|
|
||||||
.pill{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:var(--panel2);border:1px solid var(--line);font-size:12.5px;color:var(--muted);white-space:nowrap}
|
|
||||||
.dot{width:8px;height:8px;border-radius:50%;background:var(--muted)}
|
|
||||||
.dot.ok{background:var(--ok)}.dot.err{background:var(--err)}.dot.warn{background:var(--warn)}
|
|
||||||
main{max-width:1060px;margin:0 auto;padding:22px 20px 60px;display:grid;gap:18px}
|
|
||||||
.card{background:rgba(22,27,34,.86);backdrop-filter:blur(3px);border:1px solid var(--line);border-radius:var(--radius);padding:18px}
|
|
||||||
.card h2{font-size:15px;margin:0 0 12px;font-weight:600}
|
|
||||||
textarea,input,select{width:100%;background:var(--bg);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:9px 11px;font:inherit}
|
|
||||||
textarea{min-height:110px;resize:vertical;font-family:Consolas,"Cascadia Mono",monospace;font-size:13px}
|
|
||||||
textarea:focus,input:focus{outline:2px solid var(--accent2);border-color:transparent}
|
|
||||||
label{display:block;font-size:12.5px;color:var(--muted);margin:0 0 5px}
|
|
||||||
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}
|
|
||||||
.row.tri{grid-template-columns:1fr 1fr 1fr}
|
|
||||||
.row3{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:12px}
|
|
||||||
button{background:var(--panel2);color:var(--text);border:1px solid var(--line);border-radius:8px;padding:8px 14px;font:inherit;cursor:pointer}
|
|
||||||
button:hover{border-color:#3b4553}
|
|
||||||
button.primary{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
|
|
||||||
button.primary:hover{background:var(--accent2)}
|
|
||||||
button.small{padding:5px 10px;font-size:13px}
|
|
||||||
button.danger{color:#ffb4ae}
|
|
||||||
.actions{display:flex;gap:10px;align-items:center;margin-top:14px;flex-wrap:wrap}
|
|
||||||
.hint{color:var(--muted);font-size:12.5px}
|
|
||||||
.jobs{display:grid;gap:12px}
|
|
||||||
.job{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}
|
|
||||||
.job .top{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
||||||
.job .title{font-weight:600;font-size:15px;word-break:break-all}
|
|
||||||
.chip{font-size:12px;padding:2px 9px;border-radius:999px;border:1px solid var(--line);color:var(--muted);text-transform:capitalize}
|
|
||||||
.chip.hecho{color:#7ee2a0;border-color:#2b6a3d}.chip.error{color:#ff9d96;border-color:#7a2d29}
|
|
||||||
.chip.enviando,.chip.extrayendo,.chip.colocando,.chip.comprobando{color:#9ec5ff;border-color:#2f5490}
|
|
||||||
.phase{color:var(--muted);font-size:13px;margin-top:6px;word-break:break-word}
|
|
||||||
.bar{height:8px;background:var(--bg);border-radius:6px;overflow:hidden;margin-top:9px;border:1px solid var(--line)}
|
|
||||||
.bar>i{display:block;height:100%;width:0;background:var(--accent);transition:width .6s}
|
|
||||||
.bar.ex>i{background:#8957e5}
|
|
||||||
.stats{display:flex;gap:16px;flex-wrap:wrap;font-size:12.5px;color:var(--muted);margin-top:6px}
|
|
||||||
.stats b{color:var(--text);font-weight:600}
|
|
||||||
.err{color:#ff9d96;font-size:13px;margin-top:8px;white-space:pre-wrap}
|
|
||||||
.result{font-size:13px;margin-top:8px;color:#7ee2a0;font-family:Consolas,monospace}
|
|
||||||
details{margin-top:8px}
|
|
||||||
summary{cursor:pointer;color:var(--muted);font-size:12.5px}
|
|
||||||
pre.log{background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:10px;max-height:240px;overflow:auto;font-size:12px;margin:8px 0 0;white-space:pre-wrap;word-break:break-all}
|
|
||||||
.empty{color:var(--muted);text-align:center;padding:26px;border:1px dashed var(--line);border-radius:var(--radius)}
|
|
||||||
dialog{background:var(--panel);color:var(--text);border:1px solid var(--line);border-radius:14px;padding:0;width:min(720px,94vw)}
|
|
||||||
dialog::backdrop{background:rgba(0,0,0,.55)}
|
|
||||||
dialog .head{padding:16px 18px;border-bottom:1px solid var(--line);display:flex;align-items:center}
|
|
||||||
dialog .head h2{margin:0;font-size:16px}
|
|
||||||
dialog .body{padding:4px 18px 18px;max-height:70vh;overflow:auto}
|
|
||||||
.check{display:flex;gap:9px;align-items:center;margin-top:12px;font-size:14px;color:var(--text)}
|
|
||||||
.check input{width:auto}
|
|
||||||
/* --- pestanas --- */
|
|
||||||
.tabs{display:flex;gap:4px;max-width:1060px;margin:14px auto 0;padding:0 20px}
|
|
||||||
.tabs button{background:transparent;border:1px solid transparent;border-radius:9px;padding:8px 14px;color:var(--muted);font-weight:600}
|
|
||||||
.tabs button:hover{color:var(--text);border-color:var(--line)}
|
|
||||||
.tabs button.on{color:var(--text);background:var(--panel2);border-color:var(--line)}
|
|
||||||
.pickrow{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
|
|
||||||
/* --- explorador --- */
|
|
||||||
.fm-head{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
||||||
.fm-head h2{margin:0}
|
|
||||||
.store{min-width:220px;text-align:right;font-size:13px;color:var(--muted)}
|
|
||||||
.store b{color:var(--text)}
|
|
||||||
.store .bar{margin-top:6px;height:6px}
|
|
||||||
.fm-bar{display:flex;gap:6px;align-items:center;margin-top:14px;flex-wrap:wrap}
|
|
||||||
.fm-bar input,.fm-bar select{width:auto}
|
|
||||||
.fm-bar #fmPath{flex:1;min-width:220px;font-family:Consolas,"Cascadia Mono",monospace;font-size:13px}
|
|
||||||
.fm-bar #fmSearch{width:170px}
|
|
||||||
.fm-bar #fmPlaces{width:150px}
|
|
||||||
.iconbtn{padding:6px 10px;min-width:34px}
|
|
||||||
.fm-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;align-items:center}
|
|
||||||
.fm-actions .sep{width:1px;height:22px;background:var(--line);margin:0 4px}
|
|
||||||
.fm-actions button[disabled]{opacity:.4;cursor:default}
|
|
||||||
.clip,.fmop{margin-top:10px;padding:9px 12px;border:1px solid #2f5490;background:rgba(61,139,253,.08);border-radius:9px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:13.5px}
|
|
||||||
.fmop .bar{flex:1;min-width:160px;margin:0}
|
|
||||||
.fmtable{width:100%;border-collapse:collapse;margin-top:12px;font-size:13.5px}
|
|
||||||
.fmtable th{color:var(--muted);font-weight:600;font-size:12px;text-align:left;padding:8px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none;white-space:nowrap}
|
|
||||||
.fmtable td{padding:7px 8px;border-bottom:1px solid rgba(42,50,61,.6);vertical-align:middle}
|
|
||||||
.fmtable tr:hover td{background:rgba(255,255,255,.025)}
|
|
||||||
.fmtable tr.sel td{background:rgba(61,139,253,.12)}
|
|
||||||
.fmtable td.num,.fmtable th.num{text-align:right;white-space:nowrap}
|
|
||||||
.fmtable td.mono{font-family:Consolas,"Cascadia Mono",monospace;color:var(--muted);font-size:12.5px}
|
|
||||||
.fmtable input[type=checkbox]{width:auto}
|
|
||||||
.fname{display:flex;align-items:center;gap:8px;cursor:pointer}
|
|
||||||
.fname svg{flex:none;opacity:.85}
|
|
||||||
.fname.dir span{color:#cfe0ff}
|
|
||||||
.linkbtn{background:none;border:none;padding:0;color:var(--accent);cursor:pointer;font-size:12.5px}
|
|
||||||
.rowact{display:flex;gap:4px;justify-content:flex-end;opacity:.5}
|
|
||||||
tr:hover .rowact{opacity:1}
|
|
||||||
.rowact button{padding:3px 8px;font-size:12px}
|
|
||||||
.acttable{width:100%;border-collapse:collapse;font-size:13.5px;margin-top:10px}
|
|
||||||
.acttable th{color:var(--muted);font-weight:600;font-size:12px;text-align:left;padding:8px;border-bottom:1px solid var(--line)}
|
|
||||||
.acttable td{padding:8px;border-bottom:1px solid rgba(42,50,61,.6);vertical-align:top}
|
|
||||||
@media(max-width:720px){.fmtable .hide-s{display:none}}
|
|
||||||
/* --- guia de bienvenida --- */
|
|
||||||
.tour-spot{position:fixed;z-index:50;border-radius:12px;box-shadow:0 0 0 9999px rgba(5,8,14,.72),0 0 0 2px var(--accent),0 0 24px rgba(61,139,253,.55);transition:all .25s ease;pointer-events:none}
|
|
||||||
.tour-spot.none{box-shadow:0 0 0 9999px rgba(5,8,14,.72);width:0!important;height:0!important}
|
|
||||||
.tour-box{position:fixed;z-index:51;width:min(380px,calc(100vw - 32px));background:var(--panel2);border:1px solid #34405a;border-radius:14px;padding:16px 18px;box-shadow:0 18px 50px rgba(0,0,0,.5);transition:top .25s ease,left .25s ease}
|
|
||||||
.tour-box h3{margin:0 0 6px;font-size:16px}
|
|
||||||
.tour-box p{margin:0 0 12px;color:#c7d0db;font-size:14px}
|
|
||||||
.tour-box .tb-foot{display:flex;align-items:center;gap:8px}
|
|
||||||
.tour-box .tb-step{color:var(--muted);font-size:12px;margin-right:auto}
|
|
||||||
.tour-block{position:fixed;inset:0;z-index:49}
|
|
||||||
.toast{position:fixed;right:18px;bottom:18px;background:var(--panel2);border:1px solid var(--line);padding:10px 14px;border-radius:10px;opacity:0;transition:.3s;max-width:420px}
|
|
||||||
.toast.show{opacity:1}
|
|
||||||
.steps{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
|
|
||||||
.step{font-size:11.5px;color:var(--muted);padding:2px 8px;border-radius:6px;background:var(--bg);border:1px solid var(--line)}
|
|
||||||
.step.on{color:#9ec5ff;border-color:#2f5490}.step.done{color:#7ee2a0;border-color:#2b6a3d}
|
|
||||||
@media(max-width:720px){.row,.row3,.row.tri{grid-template-columns:1fr}header{flex-wrap:wrap}}
|
|
||||||
.drophint{font-size:12px;opacity:.7;margin-top:6px}
|
|
||||||
.drophint.flash{opacity:1;color:#ffcf6b}
|
|
||||||
.muted{opacity:.6}
|
|
||||||
.warn{color:#ffcf6b;font-size:13px;margin-top:6px}
|
|
||||||
.chk{display:flex;gap:8px;align-items:flex-start;padding:4px 0;font-size:13px}
|
|
||||||
.chk .ok{color:#4ade80}.chk .bad{color:#f87171}
|
|
||||||
.chip.pausado{color:#ffcf6b;border-color:#6b5a2b}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<svg width="40" height="40" style="flex:none;filter:drop-shadow(0 4px 12px rgba(75,79,230,.45))" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"> <defs> <linearGradient id="lg_bg" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#3a7bff"/> <stop offset=".55" stop-color="#4b4fe6"/> <stop offset="1" stop-color="#7b3fe4"/> </linearGradient> <radialGradient id="lg_glow" cx=".3" cy=".2" r=".9"> <stop offset="0" stop-color="#fff" stop-opacity=".35"/> <stop offset=".5" stop-color="#fff" stop-opacity="0"/> </radialGradient> <linearGradient id="lg_arrow" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" stop-color="#ffffff"/> <stop offset="1" stop-color="#dfe6ff"/> </linearGradient> </defs> <rect x="8" y="8" width="240" height="240" rx="58" fill="url(#lg_bg)"/> <rect x="8" y="8" width="240" height="240" rx="58" fill="url(#lg_glow)"/> <rect x="9.5" y="9.5" width="237" height="237" rx="56.5" fill="none" stroke="#fff" stroke-opacity=".22" stroke-width="3"/> <g stroke="#fff" stroke-linecap="round" stroke-opacity=".45" stroke-width="9"> <line x1="62" y1="58" x2="62" y2="92"/> <line x1="194" y1="58" x2="194" y2="92"/> </g> <path d="M128 44 c9 0 15 6 15 15 v58 h22 c11 0 16 13 8 21 l-35 37 c-6 6-14 6-20 0 l-35-37 c-8-8-3-21 8-21 h22 v-58 c0-9 6-15 15-15z" fill="url(#lg_arrow)"/> <path d="M52 168 v18 c0 14 10 24 24 24 h104 c14 0 24-10 24-24 v-18" fill="none" stroke="#fff" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/> <circle cx="176" cy="190" r="5.5" fill="#7dffb2"/> </svg>
|
|
||||||
<div>
|
|
||||||
<h1>PS5 Directo <span id="ver" data-noi18n style="font-size:12px;font-weight:400;opacity:.55"></span></h1>
|
|
||||||
<div class="sub">Envía archivos a tu consola por la red local, sin pasar por el disco del PC</div>
|
|
||||||
</div>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<span class="pill" id="pFtp"><span class="dot"></span>FTP</span>
|
|
||||||
<span class="pill" id="pBf"><span class="dot"></span>BFpilot</span>
|
|
||||||
<span class="pill" id="pFree" hidden></span>
|
|
||||||
<button class="small" id="btnLang" data-noi18n title="Idioma / Language"></button>
|
|
||||||
<button class="small" id="btnTour" title="Guía rápida: para qué es cada cosa">Guía</button>
|
|
||||||
<button class="small" id="btnExtHead" title="Capturar enlaces desde el navegador sin copiar y pegar">Extensión</button>
|
|
||||||
<button class="small" id="btnUpdHead" title="Buscar o instalar una versión nueva">Actualizar</button>
|
|
||||||
<button class="small" id="btnSettings">Ajustes</button>
|
|
||||||
<button class="small" id="btnQuit" title="Cierra PS5 Directo del todo">Salir</button>
|
|
||||||
</header>
|
|
||||||
<nav class="tabs" id="tabs">
|
|
||||||
<button data-tab="Transfers" class="on">Transferencias</button>
|
|
||||||
<button data-tab="Files">Archivos de la consola</button>
|
|
||||||
<button data-tab="Activity">Actividad</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main id="tabTransfers">
|
|
||||||
<section class="card">
|
|
||||||
<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>
|
|
||||||
<textarea id="lines" placeholder="https://servidor/archivo.part1.rar https://servidor/archivo.part2.rar También vale la ruta de un archivo o carpeta de tu PC (D:\Descargas\archivo.rar) o un enlace ftp:// de tu NAS"></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="pickrow">
|
|
||||||
<button class="small" id="btnPickFiles">Elegir archivos…</button>
|
|
||||||
<button class="small" id="btnPickFolder">Elegir carpeta…</button>
|
|
||||||
<span class="hint">Una carpeta se manda tal cual (si tiene sce_sys, como aplicación) y salta lo que ya esté igual en la consola.</span>
|
|
||||||
</div>
|
|
||||||
<div class="row tri">
|
|
||||||
<div>
|
|
||||||
<label for="pw">Contraseña del comprimido (si la tiene)</label>
|
|
||||||
<input id="pw" type="text" autocomplete="off" placeholder="Opcional. Si la dejas vacía prueba las de Ajustes y las del nombre">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="dest">Destino en la PS5</label>
|
|
||||||
<input id="dest" type="text" value="/data/homebrew" list="destList">
|
|
||||||
<datalist id="destList"><option value="/data/homebrew"></datalist>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="where">Dónde descomprimir</label>
|
|
||||||
<select id="where">
|
|
||||||
<option value="ps5">En la PS5 (BFpilot)</option>
|
|
||||||
<option value="pc">En el PC (7-Zip)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button class="primary" id="btnAdd">Añadir a la cola</button>
|
|
||||||
<span class="hint">Si pegas enlaces de varios archivos distintos, los separa solo. «En el PC» usa 7-Zip y manda el contenido ya descomprimido y comprobado (más datos por el Wi-Fi, pero sin BFpilot).</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div style="display:flex;align-items:center;margin-bottom:10px">
|
|
||||||
<h2 style="font-size:15px;margin:0">Cola</h2>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button class="small" id="btnClear">Quitar terminados</button>
|
|
||||||
</div>
|
|
||||||
<div class="jobs" id="jobs"></div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<main id="tabFiles" hidden>
|
|
||||||
<section class="card">
|
|
||||||
<div class="fm-head">
|
|
||||||
<h2>Archivos de la consola</h2>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<div class="store" id="fmStore"></div>
|
|
||||||
</div>
|
|
||||||
<div class="fm-bar">
|
|
||||||
<button class="small iconbtn" id="fmBack" title="Atrás">←</button>
|
|
||||||
<button class="small iconbtn" id="fmFwd" title="Adelante">→</button>
|
|
||||||
<button class="small iconbtn" id="fmUp" title="Subir un nivel">↑</button>
|
|
||||||
<input id="fmPath" value="/data/homebrew" spellcheck="false">
|
|
||||||
<select id="fmPlaces" title="Ir a"><option value="">Ir a…</option></select>
|
|
||||||
<input id="fmSearch" placeholder="Buscar aquí…">
|
|
||||||
<button class="small iconbtn" id="fmRefresh" title="Actualizar">⟳</button>
|
|
||||||
</div>
|
|
||||||
<div class="fm-actions">
|
|
||||||
<button class="small" id="fmMkdir">Nueva carpeta</button>
|
|
||||||
<button class="small" id="fmUpFiles">Subir archivos aquí</button>
|
|
||||||
<button class="small" id="fmUpFolder">Subir carpeta aquí</button>
|
|
||||||
<span class="sep"></span>
|
|
||||||
<button class="small" id="fmCopy" disabled>Copiar</button>
|
|
||||||
<button class="small" id="fmCut" disabled>Mover</button>
|
|
||||||
<button class="small" id="fmRename" disabled>Renombrar</button>
|
|
||||||
<button class="small" id="fmDownload" disabled>Descargar al PC</button>
|
|
||||||
<button class="small" id="fmSize" disabled>Calcular tamaño</button>
|
|
||||||
<button class="small" id="fmPerms" disabled>Permisos 777</button>
|
|
||||||
<button class="small danger" id="fmDelete" disabled>Borrar</button>
|
|
||||||
</div>
|
|
||||||
<div class="clip" id="fmClip" hidden></div>
|
|
||||||
<div class="fmop" id="fmOp" hidden></div>
|
|
||||||
<table class="fmtable">
|
|
||||||
<thead><tr>
|
|
||||||
<th style="width:28px"><input type="checkbox" id="fmAll"></th>
|
|
||||||
<th data-sort="name">Nombre</th>
|
|
||||||
<th data-sort="size" class="num">Tamaño</th>
|
|
||||||
<th data-sort="mtime" class="hide-s">Modificado</th>
|
|
||||||
<th class="hide-s">Permisos</th>
|
|
||||||
<th></th>
|
|
||||||
</tr></thead>
|
|
||||||
<tbody id="fmBody"></tbody>
|
|
||||||
</table>
|
|
||||||
<p class="hint" id="fmHint" style="margin-top:10px"></p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<main id="tabActivity" hidden>
|
|
||||||
<section class="card">
|
|
||||||
<div class="fm-head"><h2>Actividad</h2><div class="spacer"></div><button class="small" id="actClear">Borrar historial</button></div>
|
|
||||||
<p class="hint">Historial de transferencias terminadas o con error (las últimas 500).</p>
|
|
||||||
<table class="acttable"><thead><tr><th>Fecha</th><th>Nombre</th><th>Resultado</th><th class="num">Tamaño</th><th>Duración</th><th>Destino</th></tr></thead>
|
|
||||||
<tbody id="actBody"></tbody></table>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<dialog id="dlgSpeed">
|
|
||||||
<div class="head"><h2>Test de velocidad</h2><div class="spacer"></div><button class="small" id="btnSpeedClose">Cerrar</button></div>
|
|
||||||
<div class="body">
|
|
||||||
<p class="hint">Mide por separado la descarga desde el enlace, el envío del PC a la PS5 y las dos cosas a la vez (unos 30 segundos). Así se sabe de quién es la culpa cuando va lento. No se guarda nada: el archivo de prueba de la PS5 se borra al acabar.</p>
|
|
||||||
<div id="speedOut" style="margin-top:10px"></div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<dialog id="dlg">
|
|
||||||
<div class="head"><h2>Ajustes</h2><div class="spacer"></div><button class="small" id="btnClose">Cerrar</button></div>
|
|
||||||
<div class="body">
|
|
||||||
<div class="row">
|
|
||||||
<div><label>IP de la PS5</label><input id="s_ps5_ip" placeholder="por ejemplo 192.168.1.50"></div>
|
|
||||||
<div><label>Destino por defecto</label><input id="s_dest"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row3">
|
|
||||||
<div><label>Puerto FTP</label><input id="s_ftp_port" type="number"></div>
|
|
||||||
<div><label>Usuario FTP</label><input id="s_ftp_user"></div>
|
|
||||||
<div><label>Contraseña FTP</label><input id="s_ftp_pass" type="password" placeholder="(vacía)"></div>
|
|
||||||
<div><label>Puerto BFpilot</label><input id="s_bf_port" type="number"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row3">
|
|
||||||
<div><label>Conexiones por archivo</label><input id="s_streams" type="number" min="1" max="32"></div>
|
|
||||||
<div><label>Partes a la vez (1-4)</label><input id="s_parallel_files" type="number" min="1" max="4"></div>
|
|
||||||
<div><label>Trozo (MB)</label><input id="s_chunk_mb" type="number" min="1" max="64"></div>
|
|
||||||
<div><label>Memoria (MB)</label><input id="s_buffer_mb" type="number" min="32" max="2048"></div>
|
|
||||||
<div><label>Reintentos</label><input id="s_retries" type="number" min="0" max="50"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row3">
|
|
||||||
<div><label>Límite de velocidad a la PS5 (MB/s, 0 = sin límite)</label><input id="s_limit_mbps" type="number" min="0" max="1000"></div>
|
|
||||||
<div><label>Dejar siempre libre en la PS5 (GB)</label><input id="s_space_margin_gb" type="number" min="0" max="500"></div>
|
|
||||||
<div><label>Hilos al descomprimir (0 = automático, máx. 8)</label><input id="s_extract_threads" type="number" min="0" max="8"></div>
|
|
||||||
</div>
|
|
||||||
<p><button class="small" id="btnSpeedPS5">Test de velocidad PC → PS5</button> <span class="hint">Para incluir la descarga, usa "Test de velocidad" en una transferencia de la cola.</span></p>
|
|
||||||
<p class="hint">El límite se aplica al momento, sin parar nada. Si el espacio libre baja del margen, PS5 Directo para el envío o la descompresión antes de llenar la consola (0 = no vigilar). Con alojamientos que limitan conexiones usa solo 2 por archivo y baja más si el servidor protesta.</p>
|
|
||||||
<div style="margin-top:12px">
|
|
||||||
<label>Contraseñas que usas a menudo (una por línea). Se prueban solas si el trabajo no lleva contraseña.</label>
|
|
||||||
<textarea id="s_passwords" style="min-height:80px"></textarea>
|
|
||||||
</div>
|
|
||||||
<label class="check"><input type="checkbox" id="s_guess_passwords"> Probar también contraseñas sacadas del nombre del archivo (lo que va entre corchetes, como [WEB.COM])</label>
|
|
||||||
<label class="check"><input type="checkbox" id="s_delete_archives"> Borrar los comprimidos al terminar bien (de la PS5, o las partes bajadas al PC)</label>
|
|
||||||
<div style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px">
|
|
||||||
<b>Descomprimir en el PC</b>
|
|
||||||
<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><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>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>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button class="primary" id="btnSave">Guardar</button>
|
|
||||||
<button id="btnTest">Probar conexión</button>
|
|
||||||
<button id="btnHealth" class="small">Revisión completa</button>
|
|
||||||
<span class="hint" id="testOut"></span>
|
|
||||||
<div id="healthOut" style="margin-top:8px"></div>
|
|
||||||
<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:10px">
|
|
||||||
<b id="extSection">Extensión para capturar enlaces (Chrome / Edge)</b>
|
|
||||||
<p class="hint">Con ella, al pulsar Descargar en cualquier web (después del captcha), el enlace de un .rar/.7z/.zip/.pkg va directo aquí y el navegador no descarga nada. Si PS5 Directo está cerrado, la descarga sigue normal.<br>
|
|
||||||
Instalarla (una vez): 1) Abre <b>chrome://extensions</b> (o <b>edge://extensions</b>) · 2) Activa <b>Modo de desarrollador</b> · 3) <b>Cargar descomprimida</b> y elige la carpeta <b>extension</b> de PS5 Directo.</p>
|
|
||||||
<button id="btnExtFolder" class="small">Abrir la carpeta de la extensión</button> <span class="hint" id="extOut"></span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:10px">
|
|
||||||
<b>Actualizar PS5 Directo</b> <span class="hint" id="curVer"></span>
|
|
||||||
<p class="hint">Elige el .zip de la versión nueva y se instala solo (guarda una copia de la versión actual en la carpeta .backup). Se reinicia y se abre una ventana nueva.</p>
|
|
||||||
<input type="file" id="updFile" accept=".zip" hidden>
|
|
||||||
<button id="btnUpdZip" class="small">Instalar actualización (.zip)</button>
|
|
||||||
<div style="margin-top:8px"><label>GitHub para buscar actualizaciones (usuario/repositorio, opcional)</label><input id="s_update_repo" placeholder="por ejemplo: usuario/ps5directo"></div>
|
|
||||||
<button id="btnUpdCheck" class="small" style="margin-top:6px">Buscar actualización en GitHub</button>
|
|
||||||
<div id="updOut" class="hint" style="margin-top:6px"></div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:10px">
|
|
||||||
<b>Permisos de las aplicaciones</b>
|
|
||||||
<p class="hint">Lo que descomprimió BFpilot puede haber quedado sin permiso de ejecución y la PS5 no lo abre (error CE-107750-0). Esto pone permisos 777 a todas las aplicaciones en carpeta. No toca el contenido.</p>
|
|
||||||
<button id="btnPerms" class="small">Reparar permisos</button>
|
|
||||||
<div id="permsOut" class="hint" style="margin-top:8px"></div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:10px">
|
|
||||||
<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>
|
|
||||||
<button id="btnScan" class="small">Buscar restos</button>
|
|
||||||
<div id="cleanOut" style="margin-top:8px"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="hint" style="margin-top:16px">Necesitas en la PS5: el servidor FTP (puerto 2121) y <b>BFpilot</b> cargado desde Payload Manager (puerto 5905). Para ir más rápido, con Wi-Fi baja “Trozo” a 2–4 MB.</p>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
|
||||||
<script>
|
|
||||||
const I18N_EXACT={"Envía archivos a tu consola por la red local, sin pasar por el disco del PC": "Send files to your console over your local network, without going through your PC's disk", "Guía": "Guide", "Guía rápida: para qué es cada cosa": "Quick guide: what each part does", "Extensión": "Extension", "Capturar enlaces desde el navegador sin copiar y pegar": "Capture links from your browser without copy and paste", "Actualizar": "Update", "Buscar o instalar una versión nueva": "Check for or install a new version", "Ajustes": "Settings", "Salir": "Quit", "Cierra PS5 Directo del todo": "Closes PS5 Directo completely", "Nueva transferencia": "New transfer", "Enlaces directos (uno por línea). Pega todas las partes juntas: .part1.rar, .part2.rar… o .001, .002…": "Direct links (one per line). Paste all parts together: .part1.rar, .part2.rar… or .001, .002…", "https://servidor/archivo.part1.rar": "https://server/file.part1.rar", "https://servidor/archivo.part2.rar": "https://server/file.part2.rar", "También vale la ruta de un archivo de tu PC, por ejemplo: D:\\Descargas\\archivo.rar": "A file path on your PC also works, for example: D:\\Downloads\\file.rar", "Archivos de tu PC: suéltalos en esta página y los busco en tu PC, o arrástralos sobre el": "Files on your PC: drop them on this page and I'll find them on your PC, or drag them onto the", "icono de PS5 Directo": "PS5 Directo icon", ", o clic derecho →": ", or right-click →", "Enviar a → PS5 Directo": "Send to → PS5 Directo", "Contraseña del comprimido (si la tiene)": "Archive password (if any)", "Opcional. Si la dejas vacía prueba las de Ajustes y las del nombre": "Optional. If empty, it tries the ones in Settings and the one in the file name", "Destino en la PS5": "Destination on the PS5", "Dónde descomprimir": "Where to extract", "En la PS5 (BFpilot)": "On the PS5 (BFpilot)", "En el PC (7-Zip)": "On the PC (7-Zip)", "Añadir a la cola": "Add to queue", "Si pegas enlaces de varios archivos distintos, los separa solo. «En el PC» usa 7-Zip y manda el contenido ya descomprimido y comprobado (más datos por el Wi-Fi, pero sin BFpilot).": "If you paste links to several different files, it splits them automatically. \"On the PC\" uses 7-Zip and sends the content already extracted and verified (more data over Wi-Fi, but no BFpilot needed).", "Cola": "Queue", "Quitar terminados": "Clear finished", "No hay nada en la cola. Pega enlaces arriba.": "The queue is empty. Paste links above.", "Transferencia": "Transfer", "Pausar": "Pause", "Reanudar": "Resume", "Cancelar": "Cancel", "Reintentar": "Retry", "Quitar": "Remove", "Test de velocidad": "Speed test", "Mide descarga, envío y las dos cosas a la vez": "Measures download, upload and both at once", "Bajar al PC": "Download to PC", "Descomprimir y enviar": "Extract and send", "Colocar": "Place", "Listo": "Done", "Enviar": "Send", "Descomprimir": "Extract", "Enviado": "Sent", "de": "of", "Velocidad a la PS5": "Speed to PS5", "Quedan": "Remaining", "Descompresión en la PS5": "Extraction on the PS5", "Archivos y registro": "Files and log", "Copiar registro": "Copy log", "Registro copiado. Pégalo donde quieras (Ctrl+V).": "Log copied. Paste it anywhere (Ctrl+V).", "Registro copiado.": "Log copied.", "Contraseña del comprimido (vacío = probar las guardadas):": "Archive password (empty = try the saved ones):", "¿Cancelar esta transferencia? Lo ya enviado se conserva y se puede reintentar.": "Cancel this transfer? What was already sent is kept and you can retry.", "Añadido a la cola": "Added to queue", "pendiente": "pending", "comprobando": "checking", "enviando": "sending", "en cola para descomprimir": "waiting to extract", "extrayendo": "extracting", "colocando": "placing", "hecho": "done", "error": "error", "cancelado": "cancelled", "pausado": "paused", "Mide por separado la descarga desde el enlace, el envío del PC a la PS5 y las dos cosas a la vez (unos 30 segundos). Así se sabe de quién es la culpa cuando va lento. No se guarda nada: el archivo de prueba de la PS5 se borra al acabar.": "Measures separately the download from the link, the upload from PC to PS5, and both at once (about 30 seconds), so you know what's to blame when it's slow. Nothing is kept: the test file on the PS5 is deleted at the end.", "Cerrar": "Close", "Midiendo…": "Measuring…", "Descarga sola": "Download only", "PC → PS5 solo": "PC → PS5 only", "Las dos a la vez: descarga / envío": "Both at once: download / upload", "Llegaría a la PS5 descargando y enviando": "Would reach the PS5 downloading and sending", "IP de la PS5": "PS5 IP address", "por ejemplo 192.168.1.50": "for example 192.168.1.50", "Destino por defecto": "Default destination", "Puerto FTP": "FTP port", "Usuario FTP": "FTP user", "Contraseña FTP": "FTP password", "(vacía)": "(empty)", "Puerto BFpilot": "BFpilot port", "Conexiones por archivo": "Connections per file", "Partes a la vez (1-4)": "Parts at once (1-4)", "Trozo (MB)": "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."], ["^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}."], ["^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 in, click \"Add to queue\"."], ["^Ya existe ([\\s\\S]*?)\\. No lo piso: borralo o renombralo y pulsa Reintentar\\. Lo enviado esta en ([\\s\\S]*?)$", "{0} already exists. I won't overwrite it: delete or rename it and click Retry. What was sent is in {1}"], ["^Lo usa \"([\\s\\S]*?)\" de la cola: si lo borras, esa transferencia tendrá que enviarse otra vez\\.$", "Used by \"{0}\" in the queue: if you delete it, that transfer will have to be sent again."], ["^No encuentro en la PS5 lo enviado \\(([\\s\\S]*?)\\)\\. Pulsa ReintentaLine truncated
|
|
||||||
// --- idioma: español (original) o inglés. Traduce el texto de la página y los mensajes del programa.
|
|
||||||
const LANG=(()=>{try{const v=localStorage.getItem('ps5d_lang');if(v==='es'||v==='en')return v}catch(e){}
|
|
||||||
return (navigator.language||'es').toLowerCase().startsWith('es')?'es':'en'})();
|
|
||||||
document.documentElement.lang=LANG;
|
|
||||||
const I18N_RXC=I18N_RX.map(([a,b])=>[new RegExp(a),b]);
|
|
||||||
const I18N_CACHE=new Map();
|
|
||||||
function trCore(c){
|
|
||||||
if(I18N_EXACT[c]!==undefined) return I18N_EXACT[c];
|
|
||||||
for(const [rx,en] of I18N_RXC){const m=rx.exec(c); if(m) return en.replace(/\{(\d)\}/g,(_,i)=>tr(m[+i+1]||''));}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
function tr(s){
|
|
||||||
if(LANG!=='en'||s==null) return s;
|
|
||||||
s=String(s); if(!/[a-záéíóúñ¿¡]/i.test(s)) return s;
|
|
||||||
if(I18N_CACHE.has(s)) return I18N_CACHE.get(s);
|
|
||||||
const lead=s.match(/^\s*(?:[:·—]\s+)?/)[0], trail=s.match(/\s*$/)[0];
|
|
||||||
const core=s.slice(lead.length, s.length-trail.length);
|
|
||||||
let r=core?trCore(core):'';
|
|
||||||
if(r==null){
|
|
||||||
if(core.includes('\n')) r=core.split('\n').map(trLine).join('\n');
|
|
||||||
else{
|
|
||||||
for(const sep of [' -- ',' · ']){ if(core.includes(sep)){ r=core.split(sep).map(tr).join(sep); break } }
|
|
||||||
if(r==null){ const parts=core.split(/(?<=[.!?:])\s+(?=[A-ZÁÉÍÓÚ¿¡(])/); if(parts.length>1){ const t=parts.map(tr); r=t.join(' ') } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const out=lead+(r==null?core:r)+trail;
|
|
||||||
if(I18N_CACHE.size>5000) I18N_CACHE.clear();
|
|
||||||
I18N_CACHE.set(s,out); return out;
|
|
||||||
}
|
|
||||||
function trLine(l){const m=/^(\d\d:\d\d:\d\d )([\s\S]*)$/.exec(l); return m?m[1]+tr(m[2]):tr(l)}
|
|
||||||
const I18N_SKIP={SCRIPT:1,STYLE:1,TEXTAREA:1};
|
|
||||||
function trText(n){
|
|
||||||
const p=n.parentNode; if(!p||I18N_SKIP[p.nodeName]||(p.closest&&p.closest('[data-noi18n]'))) return;
|
|
||||||
const v=n.nodeValue; const nv=(p.nodeName==='PRE')?v.split('\n').map(trLine).join('\n'):tr(v);
|
|
||||||
if(nv!==v) n.nodeValue=nv;
|
|
||||||
}
|
|
||||||
function trAttrs(el){ for(const a of ['placeholder','title']){ const v=el.getAttribute&&el.getAttribute(a); if(v){ const nv=tr(v); if(nv!==v) el.setAttribute(a,nv) } } }
|
|
||||||
function trTree(root){
|
|
||||||
if(LANG!=='en'||!root) return;
|
|
||||||
if(root.nodeType===3){trText(root);return}
|
|
||||||
if(root.nodeType!==1||I18N_SKIP[root.nodeName]||(root.closest&&root.closest('[data-noi18n]'))) return;
|
|
||||||
trAttrs(root); root.querySelectorAll('[placeholder],[title]').forEach(trAttrs);
|
|
||||||
const w=document.createTreeWalker(root,NodeFilter.SHOW_TEXT); let n; while((n=w.nextNode())) trText(n);
|
|
||||||
}
|
|
||||||
if(LANG==='en'){
|
|
||||||
trTree(document.body);
|
|
||||||
new MutationObserver(ms=>{for(const m of ms){
|
|
||||||
if(m.type==='characterData') trText(m.target);
|
|
||||||
else if(m.type==='attributes') trAttrs(m.target);
|
|
||||||
else m.addedNodes.forEach(trTree);
|
|
||||||
}}).observe(document.body,{subtree:true,childList:true,characterData:true,attributes:true,attributeFilter:['placeholder','title']});
|
|
||||||
const _c=window.confirm.bind(window), _p=window.prompt.bind(window);
|
|
||||||
window.confirm=m=>_c(tr(m)); window.prompt=(m,d)=>_p(tr(m),d);
|
|
||||||
}
|
|
||||||
function setLang(l){try{localStorage.setItem('ps5d_lang',l)}catch(e){} location.reload()}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const TOKEN = new URLSearchParams(location.hash.slice(1)).get('t') || '';
|
|
||||||
const $ = (id) => document.getElementById(id);
|
|
||||||
let state = {jobs:[], settings:{}};
|
|
||||||
let openLogs = new Set();
|
|
||||||
|
|
||||||
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');clearTimeout(t._h);t._h=setTimeout(()=>t.classList.remove('show'),3500)}
|
|
||||||
async function api(path, body){
|
|
||||||
const r = await fetch(path,{method: body===undefined?'GET':'POST',headers:{'X-Token':TOKEN,'X-UI':'1','Content-Type':'application/json'},body: body===undefined?undefined:JSON.stringify(body)});
|
|
||||||
const j = await r.json().catch(()=>({ok:false,error:'HTTP '+r.status}));
|
|
||||||
if(!r.ok || j.ok===false) throw new Error(j.error||('HTTP '+r.status));
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
function human(n){if(n==null||n<0)return '?';const u=['B','KB','MB','GB','TB'];let i=0;n=Number(n);while(n>=1024&&i<u.length-1){n/=1024;i++}return (i?n.toFixed(1):n)+' '+u[i]}
|
|
||||||
function eta(sec){if(!isFinite(sec)||sec<=0)return '';const h=Math.floor(sec/3600),m=Math.floor(sec%3600/60),s=Math.floor(sec%60);return h?`${h} h ${m} min`:m?`${m} min ${s} s`:`${s} s`}
|
|
||||||
function esc(s){return String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}
|
|
||||||
|
|
||||||
const ORDER=['comprobando','enviando','en cola para descomprimir','extrayendo','colocando','hecho'];
|
|
||||||
function pcStepsHtml(j){
|
|
||||||
// Modo "descomprimir en el PC": bajar al PC -> descomprimir y enviar -> colocar -> listo
|
|
||||||
const hasUrl=(j.sources||[]).some(x=>x.kind==='url');
|
|
||||||
const labels=(hasUrl?[['bajar','Bajar al PC']]:[]).concat([['enviar','Descomprimir y enviar'],['colocar','Colocar'],['hecho','Listo']]);
|
|
||||||
let cur = j.status==='hecho'?'hecho':(['colocando','en cola para descomprimir'].includes(j.status)?'colocar':(j.pc_step||'enviar'));
|
|
||||||
const keys=labels.map(x=>x[0]); const ci=keys.indexOf(cur);
|
|
||||||
return '<div class="steps">'+labels.map(([k,l],i)=>{const cls=j.status==='hecho'||i<ci?'done':(i===ci&&!['error','cancelado','pausado'].includes(j.status)?'on':'');return `<span class="step ${cls}">${l}</span>`}).join('')+'</div>';
|
|
||||||
}
|
|
||||||
function folderStepsHtml(j){
|
|
||||||
// carpetas y envios "tal cual": comparar -> enviar -> listo
|
|
||||||
const labels=['Comparar','Enviar','Listo'];
|
|
||||||
let ci=j.status==='hecho'?3:(['en cola para descomprimir','colocando'].includes(j.status)?2:(j.status==='enviando'&&!/^Compar/.test(j.phase||'')?1:0));
|
|
||||||
return '<div class="steps">'+labels.map((l,i)=>{const cls=j.status==='hecho'||i<ci?'done':(i===ci&&!['error','cancelado','pausado'].includes(j.status)?'on':'');return `<span class="step ${cls}">${l}</span>`}).join('')+'</div>';
|
|
||||||
}
|
|
||||||
function stepsHtml(st){
|
|
||||||
const labels=[['enviando','Enviar'],['extrayendo','Descomprimir'],['colocando','Colocar'],['hecho','Listo']];
|
|
||||||
const idx=ORDER.indexOf(st);
|
|
||||||
return '<div class="steps">'+labels.map(([k,l])=>{const ki=ORDER.indexOf(k);const cls= st==='hecho'||idx>ki?'done':(idx===ki||(k==='enviando'&&st==='comprobando')||(k==='extrayendo'&&st==='en cola para descomprimir'))?'on':'';return `<span class="step ${cls}">${l}</span>`}).join('')+'</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const logScroll = {}; // id -> {top, bottom}
|
|
||||||
function render(){
|
|
||||||
const box=$('jobs');
|
|
||||||
// si estas seleccionando texto en la cola, no redibujar (para poder copiar)
|
|
||||||
const sel=window.getSelection();
|
|
||||||
if(sel && !sel.isCollapsed && box.contains(sel.anchorNode)) return;
|
|
||||||
box.querySelectorAll('pre.log').forEach(p=>{
|
|
||||||
const id=p.closest('.job').dataset.id;
|
|
||||||
logScroll[id]={top:p.scrollTop, bottom: p.scrollHeight-p.scrollTop-p.clientHeight<20};
|
|
||||||
});
|
|
||||||
if(!state.jobs.length){box.innerHTML='<div class="empty">No hay nada en la cola. Pega enlaces arriba.</div>';return}
|
|
||||||
box.innerHTML=state.jobs.map(j=>{
|
|
||||||
const pct = j.total>0? Math.min(100, j.sent*100/j.total):0;
|
|
||||||
const sending = j.status==='enviando';
|
|
||||||
const left = sending && j.speed>0 && j.total>0 ? eta((j.total-j.sent)/j.speed):'';
|
|
||||||
const files = (j.sources||[]).map(s=>`${esc(s.name)} (${human(s.size)})`).join('<br>');
|
|
||||||
const showEx = (['extrayendo','colocando'].includes(j.status) || j.extract_pct>0) && !j.pcx;
|
|
||||||
const canCancel = !['hecho','error','cancelado','pausado'].includes(j.status);
|
|
||||||
const canRetry = ['error','cancelado'].includes(j.status);
|
|
||||||
const canPause = ['pendiente','comprobando','enviando'].includes(j.status);
|
|
||||||
const paused = j.status==='pausado';
|
|
||||||
return `<div class="job" data-id="${j.id}">
|
|
||||||
<div class="top"><span class="title" data-noi18n>${esc(j.title|| (j.sources&&j.sources[0]&&j.sources[0].name) || 'Transferencia')}</span>
|
|
||||||
<span class="chip ${esc(j.status.split(' ')[0])}">${esc(j.status)}</span><div class="spacer"></div>
|
|
||||||
${canPause?`<button class="small" data-act="pause">Pausar</button>`:''}
|
|
||||||
${paused?`<button class="small" data-act="retry">Reanudar</button>`:''}
|
|
||||||
${canCancel?`<button class="small danger" data-act="cancel">Cancelar</button>`:''}
|
|
||||||
${canRetry?`<button class="small" data-act="retry">Reintentar</button>`:''}
|
|
||||||
${(!canPause && (j.sources||[]).some(x=>x.kind==='url') && !['extrayendo','colocando'].includes(j.status))?`<button class="small" data-act="speed" title="Mide descarga, envío y las dos cosas a la vez">Test de velocidad</button>`:''}
|
|
||||||
${!canCancel?`<button class="small" data-act="remove">Quitar</button>`:''}
|
|
||||||
</div>
|
|
||||||
${(j.raw||((j.sources||[])[0]||{}).kind==='folder')?folderStepsHtml(j):(j.pc_step||j.pcx)?pcStepsHtml(j):stepsHtml(j.status)}
|
|
||||||
<div class="phase">${esc(j.phase)}</div>
|
|
||||||
<div class="bar"><i style="width:${j.status==='hecho'?100:pct.toFixed(1)}%"></i></div>
|
|
||||||
<div class="stats"><span>${esc(j.sent_label||'Enviado')} <b>${human(j.sent)}</b> de <b>${human(j.total)}</b></span>
|
|
||||||
${sending?`<span>${esc(j.speed_label||'Velocidad a la PS5')} <b>${human(j.speed)}/s</b></span>`:''}${sending&&j.dl_speed>0&&j.sources&&j.sources.some(x=>x.kind==='url')?`<span class="muted">descarga ${human(j.dl_speed)}/s</span>`:''}${left?`<span>Quedan <b>${left}</b></span>`:''}</div>
|
|
||||||
${showEx?`<div class="bar ex"><i style="width:${j.extract_pct||0}%"></i></div><div class="stats"><span>Descompresión en la PS5 <b>${j.extract_pct||0}%</b></span><span>${esc(j.extract_text||'')}</span></div>`:''}
|
|
||||||
${j.warning?`<div class="warn">${esc(j.warning)}</div>`:''}
|
|
||||||
${j.error?`<div class="err">${esc(j.error)}</div>`:''}
|
|
||||||
${(j.result||[]).length?`<div class="result" data-noi18n>${j.result.map(esc).join('<br>')}</div>`:''}
|
|
||||||
<details ${openLogs.has(j.id)?'open':''} data-log="${j.id}"><summary>Archivos y registro</summary>
|
|
||||||
<div class="hint" style="margin-top:6px" data-noi18n>${files||'—'}</div>
|
|
||||||
<pre class="log">${esc((j.log||[]).slice(-120).join('\n'))}</pre>
|
|
||||||
<button class="small" data-act="copylog" style="margin-top:6px">Copiar registro</button></details>
|
|
||||||
</div>`}).join('');
|
|
||||||
box.querySelectorAll('details').forEach(d=>d.addEventListener('toggle',()=>{
|
|
||||||
// al redibujar, un <details open> tambien dispara 'toggle': solo bajar si lo abres tu
|
|
||||||
if(d.open && !openLogs.has(d.dataset.log)){
|
|
||||||
openLogs.add(d.dataset.log);
|
|
||||||
const p=d.querySelector('pre.log'); if(p) p.scrollTop=p.scrollHeight;
|
|
||||||
} else if(!d.open) openLogs.delete(d.dataset.log);
|
|
||||||
}));
|
|
||||||
box.querySelectorAll('pre.log').forEach(p=>{
|
|
||||||
const st=logScroll[p.closest('.job').dataset.id];
|
|
||||||
p.scrollTop = (!st || st.bottom) ? p.scrollHeight : st.top; // sigue abajo si estabas abajo
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('jobs').addEventListener('click', async (ev)=>{
|
|
||||||
const b=ev.target.closest('button[data-act]'); if(!b) return;
|
|
||||||
const id=b.closest('.job').dataset.id, act=b.dataset.act; let body={};
|
|
||||||
if(act==='speed'){
|
|
||||||
const job=state.jobs.find(x=>x.id===id); const src=(job.sources||[]).find(x=>x.kind==='url');
|
|
||||||
speedTest(src?src.input:''); return;
|
|
||||||
}
|
|
||||||
if(act==='copylog'){
|
|
||||||
const job=state.jobs.find(x=>x.id===id);
|
|
||||||
const txt=[job.title, job.status, job.error||'', ...(job.sources||[]).map(s=>s.name+' ('+human(s.size)+')'), '', ...(job.log||[])].join('\n');
|
|
||||||
try{await navigator.clipboard.writeText(txt); toast('Registro copiado. Pégalo donde quieras (Ctrl+V).')}
|
|
||||||
catch(e){const t=document.createElement('textarea');t.value=txt;document.body.appendChild(t);t.select();document.execCommand('copy');t.remove();toast('Registro copiado.')}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(act==='retry'){const job=state.jobs.find(x=>x.id===id); if(job && /contrase/i.test(job.error||'')){const p=prompt('Contraseña del comprimido (vacío = probar las guardadas):',''); if(p===null) return; body.password=p}}
|
|
||||||
if(act==='cancel' && !confirm('¿Cancelar esta transferencia? Lo ya enviado se conserva y se puede reintentar.')) return;
|
|
||||||
try{await api(`/api/job/${id}/${act}`, body); refresh()}catch(e){toast(e.message)}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('btnAdd').onclick = async ()=>{
|
|
||||||
try{await api('/api/add',{lines:$('lines').value,password:$('pw').value,dest:$('dest').value,where:$('where').value});$('lines').value='';$('pw').value='';toast('Añadido a la cola');refresh()}
|
|
||||||
catch(e){toast(e.message)}
|
|
||||||
};
|
|
||||||
$('btnClear').onclick = async ()=>{await api('/api/clear',{}).catch(()=>{});refresh()};
|
|
||||||
|
|
||||||
const SKEYS=['ps5_ip','dest','extract_where','pc_temp_dir','sevenzip_path','ftp_port','ftp_user','ftp_pass','bf_port','streams','parallel_files','limit_mbps','space_margin_gb','extract_threads','update_repo','chunk_mb','buffer_mb','retries'];
|
|
||||||
$('btnSettings').onclick=()=>{const s=state.settings;SKEYS.forEach(k=>{if(k!=='ftp_pass')$('s_'+k).value=s[k]??''});$('s_ftp_pass').value='';$('s_passwords').value=(s.passwords||[]).join('\n');$('s_guess_passwords').checked=!!s.guess_passwords;$('s_delete_archives').checked=!!s.delete_archives;$('testOut').textContent='';$('dlg').showModal()};
|
|
||||||
$('btnClose').onclick=()=>$('dlg').close();
|
|
||||||
async function saveSettings(){const b={};SKEYS.forEach(k=>{const v=$('s_'+k).value;if(k==='ftp_pass'&&!v)return;b[k]=v});b.passwords=$('s_passwords').value.split('\n');b.guess_passwords=$('s_guess_passwords').checked;b.delete_archives=$('s_delete_archives').checked;await api('/api/settings',b)}
|
|
||||||
$('btnSave').onclick=async()=>{try{await saveSettings();toast('Guardado');$('dlg').close();refresh();checkConn()}catch(e){toast(e.message)}};
|
|
||||||
$('btnTest').onclick=async()=>{$('testOut').textContent='Probando…';try{await saveSettings();const r=await api('/api/test',{});$('testOut').textContent=`FTP: ${r.ftp} · BFpilot: ${r.bf}${r.free?' · libre: '+r.free:''}`;paintConn(r)}catch(e){$('testOut').textContent=e.message}};
|
|
||||||
|
|
||||||
function paintConn(r){
|
|
||||||
const set=(id,ok,txt)=>{const p=$(id);p.querySelector('.dot').className='dot '+(ok?'ok':'err');p.title=txt};
|
|
||||||
set('pFtp', r.ftp==='ok', 'FTP: '+r.ftp); set('pBf', String(r.bf).startsWith('ok'), 'BFpilot: '+r.bf);
|
|
||||||
if(r.free){$('pFree').hidden=false;$('pFree').textContent='Libre en PS5: '+r.free}
|
|
||||||
}
|
|
||||||
async function checkConn(){try{paintConn(await api('/api/test',{}))}catch(e){}}
|
|
||||||
|
|
||||||
async function refresh(){
|
|
||||||
try{const s=await api('/api/state');state=s;if(s.version){$('ver').textContent='v'+s.version;document.title='PS5 Directo v'+s.version}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';render()}
|
|
||||||
catch(e){$('jobs').innerHTML=`<div class="empty">No conecto con el programa. ¿Cerraste la ventana negra? (${esc(e.message)})</div>`}
|
|
||||||
}
|
|
||||||
$('dest').addEventListener('input',()=>$('dest').dataset.touched='1');
|
|
||||||
$('where').addEventListener('change',()=>$('where').dataset.touched='1');
|
|
||||||
// --- guia de bienvenida (la primera vez, y con el boton "Guia")
|
|
||||||
const TOUR=[
|
|
||||||
{t:null,h:'Bienvenido a PS5 Directo',p:'Envía archivos a tu consola por la red local mientras se descargan, sin pasar por el disco del PC, y los deja descomprimidos y en su sitio. Te enseño en un minuto para qué es cada cosa.'},
|
|
||||||
{t:'#pFtp',h:'Estado de la consola',p:'Estos dos puntos dicen si la consola responde. FTP es por donde se envían los archivos; BFpilot es el que descomprime y coloca. Verde = listo. Rojo = carga el payload en la PS5 o revisa la IP en Ajustes.',pad:6,with:'#pBf'},
|
|
||||||
{t:'#lines',h:'Qué quieres enviar',p:'Pega aquí los enlaces directos, uno por línea (todas las partes .part1, .part2... juntas, en cualquier orden). También vale la ruta de un archivo de tu PC, o arrastrar archivos a esta ventana.'},
|
|
||||||
{t:'#pw',h:'Contraseña',p:'Si el comprimido lleva contraseña y la sabes, escríbela. Si la dejas vacía, prueba las que guardes en Ajustes y la que venga entre corchetes en el nombre del archivo.'},
|
|
||||||
{t:'#dest',h:'Destino',p:'Dónde acaba en la consola. /data/homebrew es la carpeta que escanea ShadowMountPlus para que aparezca en el menú. También puedes elegir un USB.'},
|
|
||||||
{t:'#where',h:'Dónde descomprimir',p:'En la PS5 (BFpilot): por el Wi-Fi viaja el comprimido, más rápido. En el PC (7-Zip): descomprime aquí sobre la marcha y comprueba cada archivo; viajan más datos, pero no depende de BFpilot.'},
|
|
||||||
{t:'#btnAdd',h:'A la cola',p:'Añade la transferencia. Puedes añadir varias: mientras una se descomprime en la consola, la siguiente ya se va enviando.'},
|
|
||||||
{t:'#jobs',h:'La cola',p:'Aquí ves cada transferencia: progreso, velocidad, tiempo restante y sus pasos. Puedes Pausar y Reanudar (sigue donde iba), Reintentar si algo falla y abrir "Archivos y registro" para ver el detalle.'},
|
|
||||||
{t:'#btnExtHead',h:'Extensión del navegador',p:'Opcional. Con ella, al pulsar Descargar en una web, el enlace llega aquí directo, sin copiar y pegar. Este botón abre su carpeta y las instrucciones.'},
|
|
||||||
{t:'#btnUpdHead',h:'Actualizar',p:'Busca la versión nueva e instálala con un clic. Guarda una copia de la anterior por si acaso.'},
|
|
||||||
{t:'#btnSettings',h:'Ajustes',p:'Lo primero: la IP de tu PS5. Ahí están también el límite de velocidad, el espacio que dejar libre, "Revisión completa" (comprueba que todo funciona), "Reparar permisos" y "Limpieza".'},
|
|
||||||
{t:'#tabs',h:'Pestañas',p:'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. Actividad guarda el historial.'},
|
|
||||||
{t:'#btnTour',h:'¡Listo!',p:'Puedes volver a ver esta guía cuando quieras con este botón. Ahora vamos a poner la IP de tu PS5.'},
|
|
||||||
];
|
|
||||||
let tourI=0, tourEls=null, tourDone=null;
|
|
||||||
function tourEnd(){
|
|
||||||
if(tourEls){tourEls.forEach(e=>e.remove());tourEls=null}
|
|
||||||
window.removeEventListener('resize',tourPlace);
|
|
||||||
api('/api/settings',{tour_done:true}).catch(()=>{});
|
|
||||||
if(state.settings) state.settings.tour_done=true;
|
|
||||||
const cb=tourDone; tourDone=null; if(cb) cb();
|
|
||||||
}
|
|
||||||
function tourPlace(){
|
|
||||||
if(!tourEls) return;
|
|
||||||
const [blk,spot,box]=tourEls, st=TOUR[tourI];
|
|
||||||
const el=st.t&&document.querySelector(st.t);
|
|
||||||
box.querySelector('h3').textContent=tr(st.h); box.querySelector('p').textContent=tr(st.p);
|
|
||||||
box.querySelector('.tb-step').textContent=tr((tourI+1)+' de '+TOUR.length);
|
|
||||||
box.querySelector('[data-t=back]').hidden=tourI===0;
|
|
||||||
box.querySelector('[data-t=next]').textContent=tr(tourI===TOUR.length-1?'Empezar':'Siguiente');
|
|
||||||
const bw=box.offsetWidth, bh=box.offsetHeight, W=innerWidth, H=innerHeight;
|
|
||||||
if(!el){spot.className='tour-spot none';spot.style.left=W/2+'px';spot.style.top=H/2+'px';
|
|
||||||
box.style.left=Math.max(16,(W-bw)/2)+'px';box.style.top=Math.max(16,(H-bh)/2)+'px';return}
|
|
||||||
let r=el.getBoundingClientRect();
|
|
||||||
if(st.with){const r2=document.querySelector(st.with).getBoundingClientRect();
|
|
||||||
r={left:Math.min(r.left,r2.left),top:Math.min(r.top,r2.top),right:Math.max(r.right,r2.right),bottom:Math.max(r.bottom,r2.bottom)}}
|
|
||||||
const pad=st.pad||8;
|
|
||||||
const L=r.left-pad,T=r.top-pad,Wd=(r.right-r.left)+pad*2,Ht=Math.min((r.bottom-r.top)+pad*2,H-T-8);
|
|
||||||
spot.className='tour-spot';Object.assign(spot.style,{left:L+'px',top:T+'px',width:Wd+'px',height:Math.max(Ht,24)+'px'});
|
|
||||||
let top=T+Math.max(Ht,24)+12; if(top+bh>H-12) top=Math.max(12,T-bh-12);
|
|
||||||
let left=Math.min(Math.max(12,L),W-bw-12);
|
|
||||||
box.style.left=left+'px'; box.style.top=top+'px';
|
|
||||||
}
|
|
||||||
function tourGo(i){
|
|
||||||
tourI=Math.max(0,Math.min(TOUR.length-1,i));
|
|
||||||
const st=TOUR[tourI], el=st.t&&document.querySelector(st.t);
|
|
||||||
if(el){el.scrollIntoView({block:'center',behavior:'instant'})}
|
|
||||||
requestAnimationFrame(tourPlace);
|
|
||||||
}
|
|
||||||
function startTour(onDone){
|
|
||||||
if(tourEls) return;
|
|
||||||
try{$('dlg').open&&$('dlg').close()}catch(e){}
|
|
||||||
tourDone=onDone||null;
|
|
||||||
const blk=document.createElement('div');blk.className='tour-block';
|
|
||||||
const spot=document.createElement('div');spot.className='tour-spot none';
|
|
||||||
const box=document.createElement('div');box.className='tour-box';
|
|
||||||
box.innerHTML='<h3></h3><p></p><div class="tb-foot"><span class="tb-step"></span><button class="small" data-t="skip">Saltar guía</button><button class="small" data-t="back">Atrás</button><button class="small primary" data-t="next">Siguiente</button></div>';
|
|
||||||
document.body.append(blk,spot,box); tourEls=[blk,spot,box];
|
|
||||||
box.querySelector('[data-t=skip]').onclick=tourEnd;
|
|
||||||
box.querySelector('[data-t=back]').onclick=()=>tourGo(tourI-1);
|
|
||||||
box.querySelector('[data-t=next]').onclick=()=>tourI===TOUR.length-1?tourEnd():tourGo(tourI+1);
|
|
||||||
window.addEventListener('resize',tourPlace);
|
|
||||||
tourGo(0);
|
|
||||||
}
|
|
||||||
function needIp(){ if(state.settings && !state.settings.ps5_ip){$('btnSettings').click();setTimeout(()=>{try{$('s_ps5_ip').focus()}catch(e){}},100);toast('Pon la IP de tu PS5 y pulsa Guardar')} }
|
|
||||||
$('btnTour').onclick=()=>startTour();
|
|
||||||
$('btnLang').textContent=LANG==='en'?'Español':'English';
|
|
||||||
$('btnLang').onclick=()=>setLang(LANG==='en'?'es':'en');
|
|
||||||
document.addEventListener('keydown',e=>{ if(!tourEls) return;
|
|
||||||
if(e.key==='Escape') tourEnd(); else if(e.key==='ArrowRight'||e.key==='Enter'){e.preventDefault();tourI===TOUR.length-1?tourEnd():tourGo(tourI+1)} else if(e.key==='ArrowLeft') tourGo(tourI-1)});
|
|
||||||
refresh().then(()=>{
|
|
||||||
if(state.settings && !state.settings.tour_done) startTour(needIp); else needIp();
|
|
||||||
checkUpdateQuiet();
|
|
||||||
}); checkConn();
|
|
||||||
setInterval(refresh, 1000); setInterval(checkConn, 20000);
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
['dragover','drop'].forEach(ev=>window.addEventListener(ev,async e=>{
|
|
||||||
if(!(e.dataTransfer&&[...e.dataTransfer.types].includes('Files'))) return;
|
|
||||||
e.preventDefault();
|
|
||||||
if(ev!=='drop') return;
|
|
||||||
const files=[...e.dataTransfer.files].filter(f=>f.size>0||f.type);
|
|
||||||
if(!files.length){toast('Eso parece una carpeta: suelta los archivos de dentro');return;}
|
|
||||||
const h=document.getElementById('drophint');
|
|
||||||
h.textContent='Buscando '+files.length+' archivo(s) en tu PC...';h.classList.add('flash');
|
|
||||||
try{
|
|
||||||
const r=await api('/api/locate',{files:files.map(f=>({name:f.name,size:f.size}))});
|
|
||||||
const ta=document.getElementById('lines');
|
|
||||||
const got=files.map(f=>r.found[f.name]).filter(Boolean);
|
|
||||||
if(got.length){ta.value=(ta.value.trim()?ta.value.trim()+'\n':'')+got.join('\n');}
|
|
||||||
const miss=files.filter(f=>!r.found[f.name]).map(f=>f.name);
|
|
||||||
h.textContent=got.length?('Añadidas '+got.length+' ruta(s). Revisa destino y pulsa "Añadir a la cola".'):'';
|
|
||||||
if(miss.length) h.textContent+=' No encontré: '+miss.join(', ')+'. Arrástralos sobre el icono de PS5 Directo o pega la ruta.';
|
|
||||||
}catch(err){h.textContent='No pude buscarlo: '+err.message;}
|
|
||||||
setTimeout(()=>h.classList.remove('flash'),6000);
|
|
||||||
}));
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
document.getElementById('btnQuit').addEventListener('click',async()=>{
|
|
||||||
const run=(state&&state.jobs||[]).some(j=>!['hecho','error','cancelado'].includes(j.status));
|
|
||||||
if(!confirm(run?'Hay trabajos en marcha. Si sales se paran (luego puedes darle a Reintentar y siguen donde iban). ¿Salir?':'¿Cerrar PS5 Directo?'))return;
|
|
||||||
try{await api('/api/quit',{})}catch(e){}
|
|
||||||
document.body.innerHTML='<p style="padding:40px;font-family:sans-serif">PS5 Directo se ha cerrado. Ya puedes cerrar esta ventana.</p>';
|
|
||||||
setTimeout(()=>window.close(),800);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
async function speedTest(url){
|
|
||||||
const out=document.getElementById('speedOut'); const d=document.getElementById('dlgSpeed');
|
|
||||||
out.innerHTML='<b>Midiendo…</b> (unos '+(url?30:10)+' segundos; no cierres el programa)'; d.showModal();
|
|
||||||
try{await api('/api/speedtest',{url})}catch(e){out.textContent=e.message;return}
|
|
||||||
const t0=Date.now();
|
|
||||||
while(true){
|
|
||||||
await new Promise(r=>setTimeout(r,1500));
|
|
||||||
let r; try{r=await api('/api/speedtest/result',{})}catch(e){out.textContent=e.message;return}
|
|
||||||
if(!r.running && r.result){
|
|
||||||
const x=r.result, f=v=>v==null?'—':v.toFixed(1)+' MB/s';
|
|
||||||
out.innerHTML=`<table style="width:100%;font-size:14px;border-collapse:collapse">
|
|
||||||
<tr><td>Descarga sola</td><td style="text-align:right"><b>${f(x.dl)}</b></td></tr>
|
|
||||||
<tr><td>PC → PS5 solo</td><td style="text-align:right"><b>${f(x.up)}</b></td></tr>
|
|
||||||
<tr><td>Las dos a la vez: descarga / envío</td><td style="text-align:right">${f(x.both_dl)} / ${f(x.both_up)}</td></tr>
|
|
||||||
<tr><td>Llegaría a la PS5 descargando y enviando</td><td style="text-align:right"><b>${f(x.pipe)}</b></td></tr>
|
|
||||||
</table><p style="margin-top:12px">${esc(x.verdict)}</p>${x.error?`<div class="err">${esc(x.error)}</div>`:''}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.innerHTML='<b>Midiendo…</b> '+Math.round((Date.now()-t0)/1000)+' s';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById('btnSpeedClose').onclick=()=>document.getElementById('dlgSpeed').close();
|
|
||||||
document.getElementById('btnSpeedPS5').onclick=()=>{document.getElementById('dlg').close();speedTest('')};
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// --- revision completa
|
|
||||||
$('btnHealth').onclick=async()=>{
|
|
||||||
const o=$('healthOut'); o.textContent='Revisando…';
|
|
||||||
try{await saveSettings(); const r=await api('/api/health',{});
|
|
||||||
o.innerHTML=r.checks.map(c=>`<div class="chk"><span class="${c.ok?'ok':'bad'}">${c.ok?'✔':'✘'}</span><div><b>${esc(c.name)}</b>: ${esc(c.detail)}${c.fix?`<div class="hint">${esc(c.fix)}</div>`:''}</div></div>`).join('');
|
|
||||||
}catch(e){o.textContent=e.message}
|
|
||||||
};
|
|
||||||
// --- permisos
|
|
||||||
$('btnPerms').onclick=async()=>{
|
|
||||||
const o=$('permsOut'); o.textContent='Revisando… (puede tardar un poco si hay muchas)';
|
|
||||||
try{const r=await api('/api/fixperms',{});
|
|
||||||
if(!r.results.length){o.textContent='No hay aplicaciones en carpeta.';return}
|
|
||||||
o.innerHTML=r.results.map(x=>x.error?`${esc(x.path)}: <span class="err">${esc(x.error)}</span>`:
|
|
||||||
`${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}
|
|
||||||
};
|
|
||||||
// --- limpieza
|
|
||||||
let cleanItems=[];
|
|
||||||
$('btnScan').onclick=async()=>{
|
|
||||||
const o=$('cleanOut'); o.textContent='Buscando… (puede tardar unos segundos)';
|
|
||||||
try{const r=await api('/api/cleanup/scan',{}); cleanItems=r.items;
|
|
||||||
if(!cleanItems.length){o.textContent='No hay nada que limpiar.';return}
|
|
||||||
o.innerHTML=cleanItems.map((it,i)=>`<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
|
|
||||||
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);
|
|
||||||
// --- avisos de Windows al terminar o fallar
|
|
||||||
const lastStatus={};
|
|
||||||
function notifyChanges(){
|
|
||||||
for(const j of (state.jobs||[])){
|
|
||||||
const prev=lastStatus[j.id]; lastStatus[j.id]=j.status;
|
|
||||||
if(!prev || prev===j.status) continue;
|
|
||||||
if(j.status==='hecho'||j.status==='error'){
|
|
||||||
const title=j.status==='hecho'?'Listo en la PS5':'Problema en PS5 Directo';
|
|
||||||
const body=(j.title||'Transferencia')+(j.status==='error'?': '+(j.error||'').slice(0,120):'');
|
|
||||||
try{ if('Notification' in window && Notification.permission==='granted') new Notification(tr(title),{body:tr(body),icon:'/favicon.ico'}); }catch(e){}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setInterval(notifyChanges, 1500);
|
|
||||||
document.addEventListener('click',()=>{try{if('Notification' in window && Notification.permission==='default') Notification.requestPermission()}catch(e){}},{once:true});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// --- enlaces que llegan de la extension del navegador
|
|
||||||
let taking=false;
|
|
||||||
async function takeInbox(){
|
|
||||||
if(taking) return; taking=true;
|
|
||||||
try{const r=await api('/api/inbox/take',{}); const ta=$('lines');
|
|
||||||
const have=new Set(ta.value.split('\n').map(x=>x.trim()).filter(Boolean));
|
|
||||||
const add=r.items.filter(i=>!have.has(i.url));
|
|
||||||
if(add.length){ta.value=(ta.value.trim()?ta.value.trim()+'\n':'')+add.map(i=>i.url).join('\n');
|
|
||||||
toast('Recibido de la extensión: '+add.map(i=>i.name||'enlace').join(', ')+'. Cuando estén todas las partes, pulsa "Añadir a la cola".');
|
|
||||||
ta.scrollIntoView({block:'center'});}
|
|
||||||
}catch(e){} finally{taking=false}
|
|
||||||
}
|
|
||||||
// --- extension
|
|
||||||
$('btnExtFolder').onclick=async()=>{try{const r=await api('/api/open_ext_folder',{});$('extOut').textContent=r.ok?('Carpeta: '+r.path):('Está en: '+r.path)}catch(e){$('extOut').textContent=e.message}};
|
|
||||||
// --- actualizar
|
|
||||||
$('btnSettings').addEventListener('click',()=>{$('curVer').textContent=state.version?('(tienes la v'+state.version+')'):''});
|
|
||||||
$('btnUpdZip').onclick=()=>$('updFile').click();
|
|
||||||
$('updFile').onchange=async()=>{
|
|
||||||
const f=$('updFile').files[0]; if(!f) return; const o=$('updOut');
|
|
||||||
if(!confirm('¿Instalar '+f.name+'? PS5 Directo se reiniciará.')) {$('updFile').value='';return}
|
|
||||||
o.textContent='Instalando…';
|
|
||||||
const b64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(String(r.result).split(',')[1]);r.onerror=rej;r.readAsDataURL(f)});
|
|
||||||
try{const r=await api('/api/update/zip',{data:b64}); afterUpdate(r.version)}catch(e){o.textContent=e.message;toast(e.message)}
|
|
||||||
$('updFile').value='';
|
|
||||||
};
|
|
||||||
$('btnUpdCheck').onclick=async()=>{
|
|
||||||
const o=$('updOut'); o.textContent='Buscando…';
|
|
||||||
try{await saveSettings(); const r=await api('/api/update/check',{});
|
|
||||||
if(!r.ok){o.textContent=r.error;return}
|
|
||||||
if(!r.newer){o.textContent='Ya tienes la última (v'+r.current+').';return}
|
|
||||||
o.innerHTML='Hay una versión nueva: <b>v'+esc(r.latest)+'</b> (tienes la v'+esc(r.current)+'). <button class="small" id="btnUpdGo">Actualizar ahora</button>';
|
|
||||||
$('btnUpdGo').onclick=async()=>{o.textContent='Descargando e instalando…';try{const x=await api('/api/update/github',{});afterUpdate(x.version)}catch(e){o.textContent=e.message}};
|
|
||||||
}catch(e){o.textContent=e.message}
|
|
||||||
};
|
|
||||||
// --- botones de la cabecera
|
|
||||||
function jobsRunning(){return (state.jobs||[]).some(j=>['comprobando','enviando','extrayendo','colocando'].includes(j.status))}
|
|
||||||
$('btnExtHead').onclick=async()=>{
|
|
||||||
$('btnSettings').click();
|
|
||||||
setTimeout(()=>$('extSection').scrollIntoView({block:'start'}),50);
|
|
||||||
try{await $('btnExtFolder').onclick()}catch(e){}
|
|
||||||
};
|
|
||||||
let latestSeen=null;
|
|
||||||
async function checkUpdateQuiet(){
|
|
||||||
if(!state.repo) return;
|
|
||||||
try{const r=await api('/api/update/check',{}); if(r.ok&&r.newer){latestSeen=r.latest;$('btnUpdHead').textContent='Actualizar · v'+r.latest;$('btnUpdHead').classList.add('primary')}}catch(e){}
|
|
||||||
}
|
|
||||||
$('btnUpdHead').onclick=async()=>{
|
|
||||||
if(jobsRunning() && !confirm('Hay transferencias en marcha. Actualizar reinicia el programa y tendrás que pulsar Reintentar en ellos (siguen donde iban). ¿Seguir?')) return;
|
|
||||||
if(!state.repo){
|
|
||||||
toast('Elige el .zip de la versión nueva');
|
|
||||||
$('updFile').click(); return;
|
|
||||||
}
|
|
||||||
try{const r=await api('/api/update/check',{});
|
|
||||||
if(!r.ok){toast(r.error);return}
|
|
||||||
if(!r.newer){
|
|
||||||
if(confirm('Ya tienes la última (v'+r.current+'). ¿Quieres instalar una desde un .zip?')) $('updFile').click();
|
|
||||||
return}
|
|
||||||
if(!confirm('Hay una versión nueva: v'+r.latest+' (tienes la v'+r.current+').'+(r.notes?'\n\n'+r.notes.slice(0,600):'')+'\n\n¿Instalarla ahora?')) return;
|
|
||||||
toast('Descargando e instalando…');
|
|
||||||
const x=await api('/api/update/github',{}); afterUpdate(x.version);
|
|
||||||
}catch(e){toast(e.message)}
|
|
||||||
};
|
|
||||||
function afterUpdate(v){
|
|
||||||
document.body.innerHTML='<div style="padding:40px;font-family:system-ui,sans-serif;color:#ddd;background:#0d1117;min-height:100vh"><h2>Actualizado a la v'+esc(v)+'</h2><p>PS5 Directo se está reiniciando y abrirá una ventana nueva en unos segundos. Ya puedes cerrar esta.</p></div>';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// ===================== pestañas =====================
|
|
||||||
let curTab='Transfers';
|
|
||||||
function showTab(t){
|
|
||||||
curTab=t;
|
|
||||||
['Transfers','Files','Activity'].forEach(x=>{$('tab'+x).hidden=(x!==t)});
|
|
||||||
document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.tab===t));
|
|
||||||
try{localStorage.setItem('ps5d_tab',t)}catch(e){}
|
|
||||||
if(t==='Files'&&!fm.loaded) fmGo(fm.path,true);
|
|
||||||
if(t==='Activity') loadActivity();
|
|
||||||
}
|
|
||||||
document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>showTab(b.dataset.tab));
|
|
||||||
|
|
||||||
// ===================== elegir archivos / carpeta del PC =====================
|
|
||||||
async function pick(kind){
|
|
||||||
const title=kind==='folder'?tr('Elige la carpeta que quieres enviar'):tr('Elige los archivos que quieres enviar');
|
|
||||||
toast(tr('Se ha abierto la ventana para elegir (puede salir detrás)'));
|
|
||||||
const r=await api('/api/pick',{kind,title});
|
|
||||||
if(!r.ok){toast(r.error);return []}
|
|
||||||
return r.paths||[];
|
|
||||||
}
|
|
||||||
$('btnPickFiles').onclick=async()=>{const ps=await pick('files'); if(ps.length){const ta=$('lines');ta.value=(ta.value.trim()?ta.value.trim()+'\n':'')+ps.join('\n');toast(tr('Añadidas {0} ruta(s). Revisa destino y pulsa "Añadir a la cola".').replace('{0}',ps.length))}};
|
|
||||||
$('btnPickFolder').onclick=async()=>{const ps=await pick('folder'); if(ps.length){const ta=$('lines');ta.value=(ta.value.trim()?ta.value.trim()+'\n':'')+ps.join('\n');toast(tr('Añadidas {0} ruta(s). Revisa destino y pulsa "Añadir a la cola".').replace('{0}',ps.length))}};
|
|
||||||
|
|
||||||
// ===================== explorador de la consola =====================
|
|
||||||
const fm={path:'/data/homebrew',entries:[],back:[],fwd:[],sel:new Set(),sizes:{},sort:'name',asc:true,clip:null,loaded:false,busy:false};
|
|
||||||
const ICON_DIR='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#7fb0ff" stroke-width="2" stroke-linejoin="round"><path d="M3 6.5A1.5 1.5 0 0 1 4.5 5h4.2l2 2.2h8.8A1.5 1.5 0 0 1 21 8.7v9.8A1.5 1.5 0 0 1 19.5 20h-15A1.5 1.5 0 0 1 3 18.5z"/></svg>';
|
|
||||||
const ICON_FILE='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#8b97a5" stroke-width="2" stroke-linejoin="round"><path d="M6 3h8l5 5v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v5h5"/></svg>';
|
|
||||||
function joinPath(a,b){return (a.replace(/\/+$/,'')+'/'+b).replace(/\/+/g,'/')}
|
|
||||||
function parentOf(p){const x=p.replace(/\/+$/,'').split('/');x.pop();return x.join('/')||'/'}
|
|
||||||
function fmtDate(t){if(!t)return '';const d=new Date(t*1000);return d.toLocaleDateString()+' '+d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}
|
|
||||||
function fmtMode(m){return m==null?'':(m&0o777).toString(8)}
|
|
||||||
async function fmGo(path,noHist){
|
|
||||||
path=(path||'/').trim()||'/';
|
|
||||||
const hint=$('fmHint'); hint.textContent=tr('Cargando…');
|
|
||||||
let r; try{r=await api('/api/fm/list',{path})}catch(e){hint.textContent=e.message;return}
|
|
||||||
if(!r.ok){hint.textContent=r.error;return}
|
|
||||||
if(!noHist && fm.path!==r.path){fm.back.push(fm.path);fm.fwd=[]}
|
|
||||||
fm.path=r.path; fm.entries=r.entries; fm.sel.clear(); fm.loaded=true;
|
|
||||||
$('fmPath').value=fm.path;
|
|
||||||
if(r.total){const used=r.total-r.free; $('fmStore').innerHTML=`<b>${human(r.free)}</b> ${esc(tr('libres de'))} ${human(r.total)}<div class="bar"><i style="width:${Math.min(100,used*100/r.total).toFixed(1)}%"></i></div>`}
|
|
||||||
else $('fmStore').textContent='';
|
|
||||||
hint.textContent=r.via==='ftp'?tr('BFpilot no responde: solo lectura por FTP. Cárgalo para copiar, mover, borrar o calcular tamaños.'):'';
|
|
||||||
fmRender();
|
|
||||||
}
|
|
||||||
function fmRender(){
|
|
||||||
const q=$('fmSearch').value.trim().toLowerCase();
|
|
||||||
let list=fm.entries.filter(e=>!q||e.name.toLowerCase().includes(q));
|
|
||||||
const k=fm.sort, dir=fm.asc?1:-1;
|
|
||||||
list.sort((a,b)=>{ if(a.dir!==b.dir) return a.dir?-1:1;
|
|
||||||
let va=k==='size'?(a.dir?(fm.sizes[joinPath(fm.path,a.name)]??-1):a.size):k==='mtime'?a.mtime:a.name.toLowerCase();
|
|
||||||
let vb=k==='size'?(b.dir?(fm.sizes[joinPath(fm.path,b.name)]??-1):b.size):k==='mtime'?b.mtime:b.name.toLowerCase();
|
|
||||||
return (va>vb?1:va<vb?-1:0)*dir});
|
|
||||||
const body=$('fmBody');
|
|
||||||
if(!list.length){body.innerHTML=`<tr><td colspan="6" class="hint" style="padding:18px;text-align:center">${esc(tr(q?'Nada coincide con la búsqueda.':'Carpeta vacía.'))}</td></tr>`}
|
|
||||||
else body.innerHTML=list.map(e=>{
|
|
||||||
const full=joinPath(fm.path,e.name), sz=e.dir?fm.sizes[full]:e.size;
|
|
||||||
return `<tr data-p="${esc(full)}" data-dir="${e.dir?1:0}" class="${fm.sel.has(full)?'sel':''}">
|
|
||||||
<td><input type="checkbox" ${fm.sel.has(full)?'checked':''}></td>
|
|
||||||
<td><div class="fname ${e.dir?'dir':''}" data-noi18n>${e.dir?ICON_DIR:ICON_FILE}<span>${esc(e.name)}</span></div></td>
|
|
||||||
<td class="num">${sz!=null?human(sz):(e.dir?`<button class="linkbtn" data-a="du">${esc(tr('calcular'))}</button>`:'')}</td>
|
|
||||||
<td class="hide-s mono">${fmtDate(e.mtime)}</td>
|
|
||||||
<td class="hide-s mono">${fmtMode(e.mode)}</td>
|
|
||||||
<td><div class="rowact"><button class="small" data-a="ren">${esc(tr('Renombrar'))}</button><button class="small" data-a="dl">${esc(tr('Descargar'))}</button><button class="small danger" data-a="del">${esc(tr('Borrar'))}</button></div></td></tr>`}).join('');
|
|
||||||
$('fmAll').checked=list.length>0&&list.every(e=>fm.sel.has(joinPath(fm.path,e.name)));
|
|
||||||
fmButtons();
|
|
||||||
}
|
|
||||||
function fmButtons(){
|
|
||||||
const n=fm.sel.size;
|
|
||||||
['fmCopy','fmCut','fmDownload','fmSize','fmPerms','fmDelete'].forEach(id=>$(id).disabled=n===0||fm.busy);
|
|
||||||
$('fmRename').disabled=n!==1||fm.busy;
|
|
||||||
const c=$('fmClip');
|
|
||||||
if(fm.clip){c.hidden=false;c.innerHTML=`<span>${esc(tr(fm.clip.op==='copy'?'Para copiar:':'Para mover:'))} <b data-noi18n>${fm.clip.paths.length===1?esc(fm.clip.paths[0].split('/').pop()):fm.clip.paths.length+' '+esc(tr('elementos'))}</b></span><button class="small primary" id="fmPaste">${esc(tr('Pegar aquí'))}</button><button class="small" id="fmClipX">${esc(tr('Cancelar'))}</button>`;
|
|
||||||
$('fmPaste').onclick=fmPaste; $('fmClipX').onclick=()=>{fm.clip=null;fmButtons()}}
|
|
||||||
else c.hidden=true;
|
|
||||||
}
|
|
||||||
$('fmBody').addEventListener('click',async ev=>{
|
|
||||||
const tr_=ev.target.closest('tr[data-p]'); if(!tr_) return;
|
|
||||||
const p=tr_.dataset.p, isDir=tr_.dataset.dir==='1';
|
|
||||||
const a=ev.target.closest('[data-a]');
|
|
||||||
if(a){ev.stopPropagation();
|
|
||||||
if(a.dataset.a==='du') return fmDu([p]);
|
|
||||||
if(a.dataset.a==='ren') return fmRename(p);
|
|
||||||
if(a.dataset.a==='dl') return fmOpStart('/api/fm/download',{paths:[p]});
|
|
||||||
if(a.dataset.a==='del') return fmDelete([p]);
|
|
||||||
}
|
|
||||||
if(ev.target.type==='checkbox'){ ev.target.checked?fm.sel.add(p):fm.sel.delete(p); tr_.classList.toggle('sel',ev.target.checked); fmButtons(); return }
|
|
||||||
if(ev.target.closest('.fname') && isDir) return fmGo(p);
|
|
||||||
if(fm.sel.has(p)) fm.sel.delete(p); else fm.sel.add(p);
|
|
||||||
fmRender();
|
|
||||||
});
|
|
||||||
$('fmBody').addEventListener('dblclick',ev=>{const tr_=ev.target.closest('tr[data-p]'); if(tr_&&tr_.dataset.dir==='1') fmGo(tr_.dataset.p)});
|
|
||||||
$('fmAll').onchange=()=>{const q=$('fmSearch').value.trim().toLowerCase();
|
|
||||||
fm.entries.filter(e=>!q||e.name.toLowerCase().includes(q)).forEach(e=>{const p=joinPath(fm.path,e.name); $('fmAll').checked?fm.sel.add(p):fm.sel.delete(p)}); fmRender()};
|
|
||||||
document.querySelectorAll('.fmtable th[data-sort]').forEach(th=>th.onclick=()=>{const k=th.dataset.sort; fm.asc=fm.sort===k?!fm.asc:true; fm.sort=k; fmRender()});
|
|
||||||
$('fmSearch').addEventListener('input',fmRender);
|
|
||||||
$('fmPath').addEventListener('keydown',e=>{if(e.key==='Enter') fmGo($('fmPath').value)});
|
|
||||||
$('fmRefresh').onclick=()=>fmGo(fm.path,true);
|
|
||||||
$('fmUp').onclick=()=>fmGo(parentOf(fm.path));
|
|
||||||
$('fmBack').onclick=()=>{if(!fm.back.length)return; fm.fwd.push(fm.path); fmGo(fm.back.pop(),true)};
|
|
||||||
$('fmFwd').onclick=()=>{if(!fm.fwd.length)return; fm.back.push(fm.path); fmGo(fm.fwd.pop(),true)};
|
|
||||||
$('fmPlaces').onchange=()=>{const v=$('fmPlaces').value; $('fmPlaces').value=''; if(v) fmGo(v)};
|
|
||||||
async function fmPlaces(){
|
|
||||||
const base=['/data/homebrew','/data','/data/pkg','/data/ps5directo','/mnt'];
|
|
||||||
try{const r=await api('/api/places',{}); (r.dests||[]).forEach(d=>{if(!base.includes(d))base.push(d)})}catch(e){}
|
|
||||||
$('fmPlaces').innerHTML=`<option value="">${esc(tr('Ir a…'))}</option>`+base.map(d=>`<option value="${esc(d)}">${esc(d)}</option>`).join('');
|
|
||||||
}
|
|
||||||
fmPlaces();
|
|
||||||
function selList(){return [...fm.sel]}
|
|
||||||
$('fmCopy').onclick=()=>{fm.clip={op:'copy',paths:selList()};fm.sel.clear();fmRender();toast(tr('Ve a la carpeta de destino y pulsa "Pegar aquí"'))};
|
|
||||||
$('fmCut').onclick=()=>{fm.clip={op:'move',paths:selList()};fm.sel.clear();fmRender();toast(tr('Ve a la carpeta de destino y pulsa "Pegar aquí"'))};
|
|
||||||
$('fmRename').onclick=()=>fmRename(selList()[0]);
|
|
||||||
$('fmDownload').onclick=()=>fmOpStart('/api/fm/download',{paths:selList()});
|
|
||||||
$('fmSize').onclick=()=>fmDu(selList().filter(p=>{const e=fm.entries.find(x=>joinPath(fm.path,x.name)===p);return e&&e.dir}));
|
|
||||||
$('fmPerms').onclick=()=>fmOpStart('/api/fm/chmod',{paths:selList()});
|
|
||||||
$('fmDelete').onclick=()=>fmDelete(selList());
|
|
||||||
$('fmMkdir').onclick=async()=>{const n=prompt(tr('Nombre de la carpeta nueva:'),''); if(!n) return;
|
|
||||||
const r=await api('/api/fm/mkdir',{path:joinPath(fm.path,n)}).catch(e=>({ok:false,error:e.message})); if(!r.ok) return toast(r.error); fmGo(fm.path,true)};
|
|
||||||
async function fmRename(p){const old=p.split('/').pop(); const n=prompt(tr('Nuevo nombre:'),old); if(!n||n===old) return;
|
|
||||||
const r=await api('/api/fm/rename',{src:p,dst:joinPath(parentOf(p),n)}).catch(e=>({ok:false,error:e.message})); if(!r.ok) return toast(r.error); fmGo(fm.path,true)}
|
|
||||||
async function fmDu(paths){for(const p of paths){fm.sizes[p]=undefined; try{const r=await api('/api/fm/du',{path:p}); if(r.ok) fm.sizes[p]=r.size; else toast(r.error)}catch(e){toast(e.message)} fmRender()}}
|
|
||||||
function fmDelete(paths){if(!paths.length) return;
|
|
||||||
const names=paths.map(p=>p.split('/').pop()); const list=names.slice(0,6).join('\n')+(names.length>6?'\n…':'');
|
|
||||||
if(!confirm(tr('¿Borrar {0} elemento(s) de la consola? No se puede deshacer.').replace('{0}',paths.length)+'\n\n'+list)) return;
|
|
||||||
fmOpStart('/api/fm/delete',{paths})}
|
|
||||||
async function fmPaste(){if(!fm.clip) return; const c=fm.clip;
|
|
||||||
const r=await fmOpStart('/api/fm/paste',{op:c.op,srcs:c.paths,dst:fm.path}); if(r) {fm.clip=null; fmButtons()}}
|
|
||||||
async function fmUpload(kind){const ps=await pick(kind); if(!ps.length) return;
|
|
||||||
try{await api('/api/add',{lines:ps.join('\n'),dest:fm.path,raw:true}); toast(tr('Añadido a la cola: se sube tal cual a {0}').replace('{0}',fm.path)); refresh()}catch(e){toast(e.message)}}
|
|
||||||
$('fmUpFiles').onclick=()=>fmUpload('files');
|
|
||||||
$('fmUpFolder').onclick=()=>fmUpload('folder');
|
|
||||||
// operaciones largas (copiar, mover, borrar, bajar, permisos) con progreso
|
|
||||||
const OPNAME={copy:'Copiando',move:'Moviendo',delete:'Borrando',download:'Descargando al PC',chmod:'Poniendo permisos'};
|
|
||||||
let fmPoll=null;
|
|
||||||
async function fmOpStart(url,body){
|
|
||||||
try{const r=await api(url,body); if(!r.ok){toast(r.error);return false}}catch(e){toast(e.message);return false}
|
|
||||||
fm.busy=true; fmButtons(); fmWatch(); return true;
|
|
||||||
}
|
|
||||||
function fmWatch(){clearInterval(fmPoll); fmPoll=setInterval(fmPollOp,900); fmPollOp()}
|
|
||||||
async function fmPollOp(){
|
|
||||||
let r; try{r=await api('/api/fm/op',{})}catch(e){return}
|
|
||||||
const o=r.op, box=$('fmOp');
|
|
||||||
if(!o.op){box.hidden=true;return}
|
|
||||||
box.hidden=false;
|
|
||||||
const pct=o.bytes_total>0?Math.min(100,o.bytes_done*100/o.bytes_total):(o.items_total?o.items_done*100/o.items_total:0);
|
|
||||||
if(o.busy){
|
|
||||||
box.innerHTML=`<b>${esc(tr(OPNAME[o.op]||o.op))}</b> <span class="hint" data-noi18n>${esc((o.current||'').split('/').pop())}</span>
|
|
||||||
<div class="bar"><i style="width:${pct.toFixed(1)}%"></i></div>
|
|
||||||
<span class="hint">${o.bytes_total?human(o.bytes_done)+' / '+human(o.bytes_total):(o.items_done+' / '+o.items_total)}</span>
|
|
||||||
<button class="small" id="fmOpCancel">${esc(tr('Cancelar'))}</button>`;
|
|
||||||
$('fmOpCancel').onclick=()=>api('/api/fm/cancel',{});
|
|
||||||
} else {
|
|
||||||
clearInterval(fmPoll); fm.busy=false; fmButtons();
|
|
||||||
box.innerHTML=o.error?`<span class="err" style="margin:0">${esc(o.error)}</span><button class="small" id="fmOpOk">${esc(tr('Cerrar'))}</button>`
|
|
||||||
:`<span>✔ ${esc(o.message||tr('Hecho'))}</span>${o.op==='download'?`<button class="small" id="fmOpenLocal">${esc(tr('Abrir carpeta'))}</button>`:''}<button class="small" id="fmOpOk">${esc(tr('Cerrar'))}</button>`;
|
|
||||||
$('fmOpOk').onclick=()=>{box.hidden=true};
|
|
||||||
if($('fmOpenLocal')) $('fmOpenLocal').onclick=()=>api('/api/fm/open_local',{});
|
|
||||||
if(o.op!=='download') fmGo(fm.path,true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmWatch();
|
|
||||||
|
|
||||||
// ===================== actividad =====================
|
|
||||||
async function loadActivity(){
|
|
||||||
let r; try{r=await api('/api/history',{})}catch(e){return}
|
|
||||||
const items=r.items||[];
|
|
||||||
$('actBody').innerHTML=items.length?items.map(h=>{
|
|
||||||
const ok=h.status==='hecho', d=new Date(h.t*1000), m=Math.floor((h.secs||0)/60);
|
|
||||||
const dur=h.secs>=3600?Math.floor(h.secs/3600)+' h '+(m%60)+' min':(m?m+' min '+(h.secs%60)+' s':(h.secs||0)+' s');
|
|
||||||
return `<tr><td class="mono">${d.toLocaleDateString()} ${d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</td>
|
|
||||||
<td data-noi18n>${esc(h.title)}</td>
|
|
||||||
<td><span class="chip ${ok?'hecho':'error'}">${esc(h.status)}</span>${h.error?`<div class="hint">${esc(h.error.slice(0,160))}</div>`:''}</td>
|
|
||||||
<td class="num">${h.size>0?human(h.size):''}</td><td>${dur}</td>
|
|
||||||
<td class="mono" data-noi18n>${esc((h.result||[]).join(', ')||h.dest||'')}</td></tr>`}).join('')
|
|
||||||
:`<tr><td colspan="6" class="hint" style="padding:18px;text-align:center">${esc(tr('Todavía no hay nada en el historial.'))}</td></tr>`;
|
|
||||||
}
|
|
||||||
$('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){}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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}"
|
||||||
@@ -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>
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -1,30 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#3a7bff"/>
|
|
||||||
<stop offset=".55" stop-color="#4b4fe6"/>
|
|
||||||
<stop offset="1" stop-color="#7b3fe4"/>
|
|
||||||
</linearGradient>
|
|
||||||
<radialGradient id="glow" cx=".3" cy=".2" r=".9">
|
|
||||||
<stop offset="0" stop-color="#fff" stop-opacity=".35"/>
|
|
||||||
<stop offset=".5" stop-color="#fff" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient id="arrow" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#ffffff"/>
|
|
||||||
<stop offset="1" stop-color="#dfe6ff"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="8" y="8" width="240" height="240" rx="58" fill="url(#bg)"/>
|
|
||||||
<rect x="8" y="8" width="240" height="240" rx="58" fill="url(#glow)"/>
|
|
||||||
<rect x="9.5" y="9.5" width="237" height="237" rx="56.5" fill="none" stroke="#fff" stroke-opacity=".22" stroke-width="3"/>
|
|
||||||
<!-- estelas de velocidad -->
|
|
||||||
<g stroke="#fff" stroke-linecap="round" stroke-opacity=".45" stroke-width="9">
|
|
||||||
<line x1="62" y1="58" x2="62" y2="92"/>
|
|
||||||
<line x1="194" y1="58" x2="194" y2="92"/>
|
|
||||||
</g>
|
|
||||||
<!-- flecha -->
|
|
||||||
<path d="M128 44 c9 0 15 6 15 15 v58 h22 c11 0 16 13 8 21 l-35 37 c-6 6-14 6-20 0 l-35-37 c-8-8-3-21 8-21 h22 v-58 c0-9 6-15 15-15z" fill="url(#arrow)"/>
|
|
||||||
<!-- bandeja / consola -->
|
|
||||||
<path d="M52 168 v18 c0 14 10 24 24 24 h104 c14 0 24-10 24-24 v-18" fill="none" stroke="#fff" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<circle cx="176" cy="190" r="5.5" fill="#7dffb2"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "PS5 Directo - Capturar enlaces",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Cuando descargas un .rar/.7z/.zip/.pkg, manda el enlace a PS5 Directo en vez de bajarlo al PC.",
|
|
||||||
"permissions": ["downloads", "storage", "notifications"],
|
|
||||||
"host_permissions": ["http://127.0.0.1/*"],
|
|
||||||
"background": { "service_worker": "bg.js" },
|
|
||||||
"action": { "default_popup": "popup.html", "default_title": "PS5 Directo", "default_icon": { "16": "icon16.png", "32": "icon32.png" } },
|
|
||||||
"icons": { "16": "icon16.png", "48": "icon48.png", "128": "icon128.png" }
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="es"><head><meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
body{font:14px system-ui,sans-serif;background:#0d1117;color:#e6edf3;margin:0;padding:14px;width:280px}
|
|
||||||
h1{font-size:15px;margin:0 0 10px}
|
|
||||||
.row{display:flex;align-items:center;justify-content:space-between;margin:8px 0}
|
|
||||||
.st{font-size:13px;opacity:.85}
|
|
||||||
.ok{color:#4ade80}.bad{color:#f87171}
|
|
||||||
small{opacity:.6;display:block;margin-top:10px;line-height:1.4}
|
|
||||||
button{background:#1f2937;color:#e6edf3;border:1px solid #374151;border-radius:6px;padding:4px 10px;cursor:pointer}
|
|
||||||
</style></head>
|
|
||||||
<body>
|
|
||||||
<h1>PS5 Directo</h1>
|
|
||||||
<div class="row"><label for="en" id="lblEn">Capturar enlaces</label><input type="checkbox" id="en"></div>
|
|
||||||
<div class="st" id="st">Comprobando…</div>
|
|
||||||
<div class="st" id="cnt"></div>
|
|
||||||
<div class="row"><span></span><button id="reset">Poner contador a 0</button></div>
|
|
||||||
<small id="help">Pasa el captcha y pulsa Descargar como siempre: si es un .rar, .7z, .zip, .pkg… y PS5 Directo está abierto, el enlace va a su cuadro de enlaces y el navegador no descarga nada. Si PS5 Directo está cerrado, la descarga sigue normal.</small>
|
|
||||||
<script src="popup.js"></script>
|
|
||||||
</body></html>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
const EN = !(navigator.language || "es").toLowerCase().startsWith("es");
|
|
||||||
const T = EN ? {
|
|
||||||
capture: "Capture links", checking: "Checking…", reset: "Reset counter",
|
|
||||||
help: "Pass the captcha and click Download as usual: if it's a .rar, .7z, .zip, .pkg… and PS5 Directo is open, the link goes to its link box and the browser downloads nothing. If PS5 Directo is closed, the download goes ahead normally.",
|
|
||||||
sent: "Sent", last: "last", connected: "● Connected", to: "to PS5 Directo", closed: "● PS5 Directo is not open", normal: "(downloads will work normally)"
|
|
||||||
} : {
|
|
||||||
capture: "Capturar enlaces", checking: "Comprobando…", reset: "Poner contador a 0",
|
|
||||||
help: null, sent: "Enviados", last: "último", connected: "● Conectado", to: "a PS5 Directo", closed: "● PS5 Directo no está abierto", normal: "(las descargas seguirán normales)"
|
|
||||||
};
|
|
||||||
if (EN) {
|
|
||||||
document.documentElement.lang = "en";
|
|
||||||
document.getElementById("lblEn").textContent = T.capture;
|
|
||||||
document.getElementById("st").textContent = T.checking;
|
|
||||||
document.getElementById("reset").textContent = T.reset;
|
|
||||||
document.getElementById("help").textContent = T.help;
|
|
||||||
}
|
|
||||||
async function findApp() {
|
|
||||||
for (const port of [8777, 8778, 8779]) {
|
|
||||||
try {
|
|
||||||
const r = await fetch(`http://127.0.0.1:${port}/api/ext/ping`, { cache: "no-store" });
|
|
||||||
if (r.ok) { const j = await r.json(); if (j.app === "PS5 Directo") return j; }
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
(async () => {
|
|
||||||
const cfg = await chrome.storage.local.get({ enabled: true, captured: 0, last: "" });
|
|
||||||
const en = document.getElementById("en");
|
|
||||||
en.checked = cfg.enabled;
|
|
||||||
en.onchange = () => chrome.storage.local.set({ enabled: en.checked });
|
|
||||||
document.getElementById("cnt").textContent =
|
|
||||||
cfg.captured ? `${T.sent}: ${cfg.captured}${cfg.last ? " (" + T.last + ": " + cfg.last + ")" : ""}` : "";
|
|
||||||
document.getElementById("reset").onclick = async () => {
|
|
||||||
await chrome.storage.local.set({ captured: 0, last: "" });
|
|
||||||
chrome.action.setBadgeText({ text: "" });
|
|
||||||
document.getElementById("cnt").textContent = "";
|
|
||||||
};
|
|
||||||
const app = await findApp();
|
|
||||||
const st = document.getElementById("st");
|
|
||||||
if (app) { st.innerHTML = `<span class="ok">${T.connected}</span> ${T.to} v${app.version}`; chrome.action.setBadgeText({ text: "" }); }
|
|
||||||
else st.innerHTML = `<span class="bad">${T.closed}</span> ${T.normal}`;
|
|
||||||
})();
|
|
||||||
|
Before Width: | Height: | Size: 62 KiB |
@@ -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" "$@"
|
||||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 98 KiB |