mirror of
https://github.com/jstrdev/ps5directo.git
synced 2026-10-28 17:01:37 +01:00
Versión 1.14.0
This commit is contained in:
1 parent
c48e8674d8
commit
fdc42abe0a
18 files changed
+4566
-2
No files matched your search
@@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cd /d "%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$w=New-Object -ComObject WScript.Shell; $d=(Get-Location).Path;" ^
|
||||
"foreach($f in @([Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('SendTo'))){" ^
|
||||
" $l=$w.CreateShortcut((Join-Path $f 'PS5 Directo.lnk'));" ^
|
||||
" $l.TargetPath=(Join-Path $env:WINDIR 'System32\wscript.exe');" ^
|
||||
" $l.Arguments='\"'+(Join-Path $d 'PS5 Directo.vbs')+'\"';" ^
|
||||
" $l.WorkingDirectory=$d; $l.IconLocation=(Join-Path $d 'web\ps5directo.ico');" ^
|
||||
" $l.Description='PS5 Directo'; $l.Save() }"
|
||||
if %errorlevel%==0 (
|
||||
echo Listo:
|
||||
echo - Icono "PS5 Directo" en el escritorio. Tambien puedes soltar archivos encima.
|
||||
echo - Clic derecho en archivos ^> Enviar a ^> PS5 Directo.
|
||||
) else (
|
||||
echo No se pudieron crear los accesos directos.
|
||||
)
|
||||
pause
|
||||
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title PS5 Directo
|
||||
cd /d "%~dp0"
|
||||
where py >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
py -3 app.py %*
|
||||
) else (
|
||||
python app.py %*
|
||||
)
|
||||
if %errorlevel%==3 exit /b 0
|
||||
echo.
|
||||
echo PS5 Directo se ha cerrado.
|
||||
pause
|
||||
@@ -0,0 +1,263 @@
|
||||
PS5 DIRECTO 1.14
|
||||
================
|
||||
|
||||
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
|
||||
(nada se guarda en el disco del PC).
|
||||
2. Une las partes troceadas (.001, .002...) en un solo archivo.
|
||||
3. Pide a BFpilot que descomprima el RAR/7z/ZIP DENTRO de la consola,
|
||||
probando las contraseñas que conozca.
|
||||
4. Busca la carpeta de la aplicación (la que tiene sce_sys) y la deja en
|
||||
/data/homebrew, donde ShadowMountPlus la detecta sola.
|
||||
5. Borra los comprimidos de la PS5.
|
||||
|
||||
Mientras una transferencia se descomprime en la PS5, la siguiente de la cola ya se
|
||||
va enviando.
|
||||
|
||||
ALTERNATIVA: DESCOMPRIMIR EN EL PC (7-Zip)
|
||||
En "Dónde descomprimir" elige "En el PC (7-Zip)" (o ponlo por defecto en
|
||||
Ajustes). Entonces:
|
||||
- Los enlaces se bajan antes al PC (carpeta temporal de Ajustes; hace
|
||||
falta sitio para el RAR, no para lo descomprimido). Tus archivos locales se
|
||||
usan tal cual y nunca se borran.
|
||||
- 7-Zip descomprime sobre la marcha y la app manda cada archivo ya
|
||||
suelto a la PS5. Lo descomprimido NO se escribe en el PC.
|
||||
- Cada archivo se comprueba con su CRC antes de darlo por bueno.
|
||||
- En la PS5 solo ocupa el contenido final (sin el RAR al lado) y se
|
||||
sabe exacto antes de empezar.
|
||||
- Pega: por el Wi-Fi viaja todo sin comprimir (puede ser 3-4 veces
|
||||
más que el RAR), así que tarda más en enviarse.
|
||||
- Pausar/Reintentar sigue por el archivo donde iba.
|
||||
Necesita 7-Zip instalado (gratis, 7-zip.org). No necesita BFpilot.
|
||||
|
||||
|
||||
LO QUE NECESITAS
|
||||
----------------
|
||||
Esta guía no explica cómo hacer el jailbreak: das por hecho que tu PS5 ya
|
||||
lo tiene y que sabes cargar payloads (.elf).
|
||||
|
||||
EN EL PC
|
||||
- Windows 10 u 11.
|
||||
- Python 3.9 o superior: https://www.python.org/downloads/
|
||||
Al instalarlo marca "Add python.exe to PATH".
|
||||
- 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:
|
||||
https://github.com/itsPLK/ps5-payload-manager )
|
||||
- Un servidor FTP. El de referencia es ftpsrv (puerto 2121):
|
||||
https://github.com/ps5-payload-dev/ftpsrv
|
||||
Muchos menús de payloads ya lo traen. Si el tuyo usa otro puerto,
|
||||
cámbialo en Ajustes.
|
||||
- BFpilot (puerto 5905): descomprime dentro de la consola, mueve y borra.
|
||||
Descarga bfpilot.elf de: https://github.com/ItsBlurf/BFpilot/releases
|
||||
Si no está cargado, el programa espera a que lo cargues. En el modo
|
||||
"Descomprimir en el PC" no hace falta.
|
||||
- ShadowMountPlus: es el que hace que las aplicaciones de /data/homebrew
|
||||
aparezcan en el menú de la PS5:
|
||||
https://github.com/drakmor/ShadowMountPlus
|
||||
|
||||
Para comprobar que todo está bien: Ajustes > "Revisión completa".
|
||||
|
||||
|
||||
SOBRE BFPILOT Y LOS PERMISOS (importante si lo usas por tu cuenta)
|
||||
-----------------------------------------------------------------
|
||||
Hasta su versión 0.4.4 al menos, cuando BFpilot descomprime un RAR, 7z o
|
||||
ZIP deja los archivos sin permiso de ejecución, y la PS5 se niega a abrir
|
||||
la aplicación (error CE-107750-0). Pasa también si descomprimes desde la web de
|
||||
BFpilot, sin PS5 Directo.
|
||||
- PS5 Directo lo arregla solo después de cada descompresión.
|
||||
- Para lo que ya tenías en la consola: Ajustes > "Reparar permisos".
|
||||
- A mano: en FileZilla, clic derecho en la carpeta de la aplicación >
|
||||
Permisos de archivo > 777 > "Recurrir a subdirectorios".
|
||||
|
||||
|
||||
PRIMERA VEZ
|
||||
-----------
|
||||
Doble clic en "Crear accesos directos.bat". Crea:
|
||||
- Un icono "PS5 Directo" en el escritorio.
|
||||
- "Enviar a > PS5 Directo" en el clic derecho de los archivos.
|
||||
Desde entonces abre PS5 Directo con el icono del escritorio: sale en su
|
||||
propia ventana, sin pestañas y sin ventana negra.
|
||||
|
||||
(Si algo falla al abrir, usa "Iniciar PS5 Directo.bat": es lo mismo pero
|
||||
con la ventana negra, donde se ven los errores. Registro sin ventana:
|
||||
%USERPROFILE%\.ps5directo\ps5directo.log)
|
||||
|
||||
Si cierras la ventana con trabajos en marcha, siguen en segundo plano.
|
||||
Vuelve a abrir PS5 Directo para verlos. El botón "Salir" lo cierra del
|
||||
todo (lo que esté a medias sigue luego con "Reintentar").
|
||||
|
||||
|
||||
CÓMO SE USA
|
||||
-----------
|
||||
1. Descomprime esta carpeta donde quieras.
|
||||
2. Abre PS5 Directo con el icono del escritorio.
|
||||
3. La primera vez se abre Ajustes: pon la IP de tu PS5 (la ves en la
|
||||
PS5 en Ajustes > Red > Ver estado de la conexión), pulsa "Guardar" y
|
||||
"Probar conexión". Los dos puntos de arriba deben ponerse en verde.
|
||||
4. Pega los enlaces directos (todas las partes juntas, en el
|
||||
orden que sea) y pulsa "Añadir a la cola".
|
||||
- Si sabes la contraseña, escríbela. Si no, prueba las de Ajustes y
|
||||
lo que vaya entre corchetes en el nombre del archivo, como [WEB.COM].
|
||||
- También vale la ruta de un archivo que ya tengas en el PC.
|
||||
|
||||
5. ARCHIVOS DEL PC SIN COPIAR RUTAS:
|
||||
- Arrastra uno o varios archivos sobre el icono del escritorio.
|
||||
- O clic derecho en los archivos > Enviar a > PS5 Directo.
|
||||
Si PS5 Directo ya está abierto, se añaden a su cola.
|
||||
- O suéltalos en la página del navegador: PS5 Directo los busca en
|
||||
tu PC por nombre y tamaño y pone sus rutas en el cuadro de enlaces.
|
||||
|
||||
|
||||
PAUSA, LÍMITE Y ESPACIO
|
||||
-----------------------
|
||||
- "Pausar" detiene el envío; "Reanudar" sigue donde iba.
|
||||
- Ajustes > Límite de velocidad (MB/s): para usar internet mientras envía.
|
||||
Se aplica al momento. 0 = sin límite.
|
||||
- Ajustes > Dejar siempre libre (GB, 25 por defecto): antes de empezar
|
||||
comprueba que cabe lo que falta por enviar, y mientras envía o
|
||||
descomprime vigila el espacio. Si baja de ese margen, lo para todo
|
||||
antes de que la PS5 se llene. Libera espacio y pulsa Reintentar.
|
||||
- Algunos alojamientos de archivos limitan las conexiones: con ellos usa
|
||||
solo 2 por archivo, y menos si el servidor empieza a dar errores.
|
||||
|
||||
|
||||
ACTUALIZAR Y EXTENSIÓN
|
||||
----------------------
|
||||
- Botón "Actualizar" (arriba): si el programa tiene un repositorio de
|
||||
GitHub configurado, busca la versión nueva y la instala. Si no, te
|
||||
deja elegir el .zip de la versión nueva. Guarda una copia de la versión
|
||||
anterior en la carpeta .backup.
|
||||
- Botón "Extensión" (arriba): abre la carpeta de la extensión para
|
||||
Chrome/Edge y las instrucciones. Con ella, al pulsar Descargar en una
|
||||
web, el enlace va directo a PS5 Directo sin copiar y pegar.
|
||||
|
||||
|
||||
TEST DE VELOCIDAD
|
||||
-----------------
|
||||
En una transferencia pausada o terminada, "Test de velocidad" mide (unos 30 s):
|
||||
descarga sola, envio PC -> PS5 solo y las dos cosas a la vez, y te dice
|
||||
de quien es la culpa cuando va lento. Tambien hay uno solo PC -> PS5 en
|
||||
Ajustes.
|
||||
|
||||
|
||||
NOVEDADES 1.14
|
||||
- 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.
|
||||
|
||||
NOVEDADES 1.13.1
|
||||
- El logo de la cabecera va dentro de la página (al actualizar desde una
|
||||
versión anterior no se copiaba y salía roto).
|
||||
|
||||
NOVEDADES 1.13
|
||||
- Icono nuevo y fondo con símbolos. Para que el icono nuevo salga en el
|
||||
escritorio, vuelve a ejecutar "Crear accesos directos.bat".
|
||||
|
||||
NOVEDADES 1.12.1
|
||||
- Textos más neutros: "transferencias" y "aplicaciones", como una
|
||||
herramienta de transferencia de archivos.
|
||||
|
||||
NOVEDADES 1.12
|
||||
- Botones "Actualizar" y "Extensión" en la pantalla principal. Si hay
|
||||
versión nueva en GitHub, el botón lo avisa.
|
||||
- La primera vez pide la IP de la PS5 (ya no trae una puesta).
|
||||
- Guía de lo que hace falta y de dónde sacarlo (arriba).
|
||||
|
||||
NOVEDADES 1.11
|
||||
- ARREGLO IMPORTANTE: lo descomprimido con BFpilot no arrancaba
|
||||
(CE-107750-0). Su unrar deja los archivos con los permisos que trae el
|
||||
RAR (666/444, sin permiso de ejecución). Ahora PS5 Directo les pone 777
|
||||
justo al colocarlos, y lo apunta en el registro.
|
||||
- Ajustes > "Reparar permisos": arregla las aplicaciones que ya tienes en la
|
||||
consola (los que pasaste antes con BFpilot) sin volver a bajarlos.
|
||||
- Modo PC: si BFpilot no está cargado, lo coloca por FTP (ya no
|
||||
hace falta BFpilot para nada en ese modo).
|
||||
|
||||
NOVEDADES 1.10.1
|
||||
- En el modo PC los pasos son "Bajar al PC → Descomprimir y enviar → Colocar
|
||||
→ Listo", y dice por qué archivo va (37 de 129...).
|
||||
|
||||
NOVEDADES 1.10
|
||||
- Nueva opción: descomprimir en el PC con 7-Zip y mandar el contenido ya
|
||||
suelto, comprobado archivo a archivo (ver arriba).
|
||||
- La "Revisión completa" dice si encuentra 7-Zip.
|
||||
|
||||
NOVEDADES 1.9
|
||||
-------------
|
||||
- EXTENSION PARA CAPTURAR ENLACES (Chrome o Edge), en la carpeta "extension":
|
||||
1. Abre chrome://extensions (o edge://extensions).
|
||||
2. Activa "Modo de desarrollador" (arriba a la derecha).
|
||||
3. "Cargar descomprimida" y elige la carpeta "extension" de PS5 Directo.
|
||||
Desde entonces, pasa el captcha y pulsa Descargar como siempre: si es un
|
||||
.rar/.7z/.zip/.pkg y PS5 Directo esta abierto, el enlace bueno (despues de
|
||||
redirecciones) aparece en el cuadro de enlaces y el navegador NO descarga
|
||||
nada. Cuando esten todas las partes, pulsa "Anadir a la cola".
|
||||
Si PS5 Directo esta cerrado, la descarga sigue normal. Se apaga y enciende
|
||||
desde su icono.
|
||||
- ACTUALIZAR: Ajustes > "Instalar actualizacion (.zip)": eliges el zip nuevo y
|
||||
se instala solo, guarda copia de la version anterior en .backup y se
|
||||
reinicia. Si algun dia lo subes a GitHub, pon "usuario/repositorio" y el
|
||||
boton "Buscar actualizacion" lo descarga de las releases.
|
||||
- La version sale arriba, junto al titulo, y en el nombre de la ventana.
|
||||
|
||||
|
||||
NOVEDADES 1.8
|
||||
-------------
|
||||
- Mientras descomprime ves "leido del RAR X de Y" (lo que BFpilot ha
|
||||
procesado del comprimido) y "descomprimido" (lo que ocupa de verdad en
|
||||
la PS5). Una aplicación en formato carpeta puede ocupar varias veces el RAR:
|
||||
Lost Records, 35 GB de RAR -> 145 GB de carpeta.
|
||||
- Las previsiones de espacio cuentan con hasta ~5 veces el tamano del RAR.
|
||||
- Si el RAR trae la aplicación dentro de una carpeta, se coloca igual como
|
||||
TITLEID-app.
|
||||
- La version aparece arriba, junto al titulo.
|
||||
|
||||
|
||||
NOVEDADES 1.7
|
||||
-------------
|
||||
- Ajustes > Revision completa: comprueba FTP, escritura, BFpilot, formatos
|
||||
y espacio, y dice que hacer si algo falla.
|
||||
- Ajustes > Limpieza de la PS5: busca restos en /data/ps5directo, ensena
|
||||
cuanto ocupan y los borra (nunca toca nada fuera de esa carpeta).
|
||||
- Aviso si el Title ID ya existe en /data/homebrew o en un USB/externo.
|
||||
- Destino: sugiere /mnt/usbX/homebrew si hay unidades conectadas.
|
||||
- Ajustes > Hilos al descomprimir (0 = automatico). Si una descompresion
|
||||
se cuelga, prueba con 1.
|
||||
- Si la descompresion no da senales de vida en 5 min, lo avisa.
|
||||
- Aviso de Windows al terminar o fallar (acepta el permiso la 1a vez).
|
||||
|
||||
|
||||
SI ALGO FALLA
|
||||
-------------
|
||||
- Se corta la red o el FTP: reintenta solo y continúa donde se quedó.
|
||||
- Cierras el programa a medias: al abrirlo, pulsa "Reintentar".
|
||||
Continúa desde lo que ya había en la consola.
|
||||
- Contraseña incorrecta: pulsa "Reintentar" y escríbela.
|
||||
No se vuelve a descargar nada.
|
||||
- "Ya existe": no pisa nada que ya tengas. Bórralo o renómbralo y
|
||||
pulsa "Reintentar".
|
||||
- Los archivos de trabajo están en /data/ps5directo (fuera de las
|
||||
carpetas que escanea ShadowMountPlus).
|
||||
|
||||
|
||||
ENLACES
|
||||
-------
|
||||
Funcionan los enlaces de descarga directa. Pixeldrain, Dropbox y algunos
|
||||
de Mediafire y Google Drive se convierten solos. Las páginas que piden
|
||||
captcha o iniciar sesión no funcionan: copia el enlace final de descarga.
|
||||
|
||||
Los .pkg se dejan en /data/pkg para instalarlos con ps5upload.
|
||||
Las imágenes (.exfat, .ffpkg, .ffpfsc) van a /data/homebrew.
|
||||
|
||||
Ajustes y cola se guardan en %USERPROFILE%\.ps5directo
|
||||
|
||||
|
||||
AVISO
|
||||
-----
|
||||
PS5 Directo es un programa independiente, sin relación con Sony ni con
|
||||
los autores de BFpilot, ftpsrv o ShadowMountPlus. Es una herramienta de
|
||||
transferencia de archivos: 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 (tus copias de seguridad y homebrew).
|
||||
Cada uno es responsable de lo que transfiere.
|
||||
@@ -0,0 +1,21 @@
|
||||
' Abre PS5 Directo sin ventana negra (en su propia ventana de aplicacion).
|
||||
' Puedes arrastrar archivos encima de este archivo o de su acceso directo.
|
||||
Set sh = CreateObject("WScript.Shell")
|
||||
Set fso = CreateObject("Scripting.FileSystemObject")
|
||||
dir = fso.GetParentFolderName(WScript.ScriptFullName)
|
||||
sh.CurrentDirectory = dir
|
||||
args = ""
|
||||
For Each a In WScript.Arguments
|
||||
args = args & " """ & a & """"
|
||||
Next
|
||||
py = ""
|
||||
If sh.Run("cmd /c where pyw >nul 2>nul", 0, True) = 0 Then
|
||||
py = "pyw -3"
|
||||
ElseIf sh.Run("cmd /c where pythonw >nul 2>nul", 0, True) = 0 Then
|
||||
py = "pythonw"
|
||||
End If
|
||||
If py = "" Then
|
||||
MsgBox "No encuentro Python. Instala Python 3 (marca 'Add to PATH') o usa 'Iniciar PS5 Directo.bat'.", 48, "PS5 Directo"
|
||||
Else
|
||||
sh.Run py & " """ & dir & "\app.py""" & args, 0, False
|
||||
End If
|
||||
@@ -1,2 +1,29 @@
|
||||
# ps5directo
|
||||
Herramienta de transferencia de archivos a PS5 por red local
|
||||
# PS5 Directo
|
||||
|
||||
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ú.
|
||||
|
||||
- Descarga en paralelo, con pausa, reanudación y límite de velocidad.
|
||||
- Vigila el espacio libre de la PS5 para no llenarla.
|
||||
- Corrige los permisos de lo descomprimido con BFpilot. Sin esa corrección no arranca (CE-107750-0).
|
||||
- Extensión opcional para Chrome/Edge que manda los enlaces directamente al programa.
|
||||
- Se actualiza desde las releases de este repositorio.
|
||||
|
||||
## Requisitos
|
||||
|
||||
- **PC:** Windows 10/11, [Python 3.9+](https://www.python.org/downloads/) y, para descomprimir en el PC, [7-Zip](https://www.7-zip.org/).
|
||||
- **PS5 con jailbreak:**
|
||||
- Servidor FTP, por ejemplo [ftpsrv](https://github.com/ps5-payload-dev/ftpsrv) en el puerto 2121.
|
||||
- [BFpilot](https://github.com/ItsBlurf/BFpilot) en el puerto 5905.
|
||||
- [ShadowMountPlus](https://github.com/drakmor/ShadowMountPlus).
|
||||
|
||||
## Uso
|
||||
|
||||
1. Descarga el `.zip` de la última [release](../../releases) y descomprímelo.
|
||||
2. Ejecuta `Crear accesos directos.bat` y abre **PS5 Directo** desde el escritorio.
|
||||
3. Pon la IP de tu PS5 en Ajustes y pega los enlaces o arrastra los archivos.
|
||||
|
||||
La guía completa está en [LEEME.txt](LEEME.txt).
|
||||
|
||||
## 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).
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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: "Enviado a PS5 Directo",
|
||||
message: (name || "Enlace") + " — 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);
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 778 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"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" }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!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">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>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>
|
||||
@@ -0,0 +1,26 @@
|
||||
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 ? `Enviados: ${cfg.captured}${cfg.last ? " (último: " + 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">● Conectado</span> a PS5 Directo v${app.version}`; chrome.action.setBadgeText({ text: "" }); }
|
||||
else st.innerHTML = '<span class="bad">● PS5 Directo no está abierto</span> (las descargas seguirán normales)';
|
||||
})();
|
||||
+626
@@ -0,0 +1,626 @@
|
||||
<!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}
|
||||
/* --- 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" 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="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>
|
||||
|
||||
<main>
|
||||
<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 de tu PC, por ejemplo: D:\Descargas\archivo.rar"></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="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>
|
||||
|
||||
<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 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 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">${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.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">${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">${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:'#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=st.h; box.querySelector('p').textContent=st.p;
|
||||
box.querySelector('.tb-step').textContent=(tourI+1)+' de '+TOUR.length;
|
||||
box.querySelector('[data-t=back]').hidden=tourI===0;
|
||||
box.querySelector('[data-t=next]').textContent=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();
|
||||
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(title,{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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Reference in new issue
Block a user