mirror of
https://github.com/jstrdev/ps5directo.git
synced 2026-10-29 04:01:51 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8113534ea | ||
|
|
5128f49f8b |
No files matched your search
@@ -1,5 +1,5 @@
|
||||
PS5 DIRECTO 1.18
|
||||
================
|
||||
PS5 DIRECTO 1.18.3
|
||||
==================
|
||||
(English: see README_EN.txt)
|
||||
|
||||
Pega los enlaces (o arrastra archivos de tu PC) y la herramienta hace todo sola:
|
||||
@@ -168,6 +168,20 @@ de quien es la culpa cuando va lento. Tambien hay uno solo PC -> PS5 en
|
||||
Ajustes.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
+11
-2
@@ -1,5 +1,5 @@
|
||||
PS5 DIRECTO 1.18 — USER GUIDE (English)
|
||||
=======================================
|
||||
PS5 DIRECTO 1.18.3 — USER GUIDE (English)
|
||||
=========================================
|
||||
|
||||
Paste links (or drag files from your PC) and the tool does the rest:
|
||||
|
||||
@@ -220,6 +220,15 @@ Settings and queue are stored in %USERPROFILE%\.ps5directo
|
||||
|
||||
WHAT'S NEW
|
||||
----------
|
||||
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
|
||||
|
||||
@@ -22,7 +22,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import engine as E # noqa: E402
|
||||
|
||||
VERSION = "1.18.1"
|
||||
VERSION = "1.18.3"
|
||||
# Repositorio de GitHub donde se publican las versiones (usuario/nombre). Si lo rellenas,
|
||||
# todas las copias buscan actualizaciones ahi sin tener que configurar nada.
|
||||
DEFAULT_REPO = "jstrdev/ps5directo"
|
||||
@@ -55,6 +55,8 @@ DEFAULTS = {
|
||||
"delete_archives": True,
|
||||
"tour_done": False, # la guia de bienvenida ya se vio
|
||||
"seen_version": "", # ultima version cuyas novedades ya se ensenaron
|
||||
"shortcut_done": False, # ya se creo el acceso directo del escritorio una vez
|
||||
"icon_ver": "", # version cuyo icono ya se dejo en la cache de la ventana
|
||||
}
|
||||
|
||||
LOCK = threading.RLock()
|
||||
@@ -2561,7 +2563,19 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if self.path == "/logo.svg":
|
||||
with open(os.path.join(HERE, "web", "logo.svg"), "rb") as f:
|
||||
return self._send(200, f.read(), "image/svg+xml")
|
||||
if self.path == "/favicon.ico":
|
||||
if self.path.startswith("/icon-") and self.path.split("?")[0].endswith(".png"):
|
||||
n = re.sub(r"\D", "", self.path.split("?")[0])
|
||||
fp = os.path.join(HERE, "web", "icon-%s.png" % n)
|
||||
if n and os.path.isfile(fp):
|
||||
with open(fp, "rb") as f:
|
||||
return self._send(200, f.read(), "image/png")
|
||||
if self.path.split("?")[0] == "/manifest.webmanifest":
|
||||
man = {"name": "PS5 Directo", "short_name": "PS5 Directo", "display": "standalone",
|
||||
"background_color": "#0f1115", "theme_color": "#0f1115", "start_url": "/",
|
||||
"icons": [{"src": "/icon-%d.png?v=%s" % (n, VERSION), "sizes": "%dx%d" % (n, n),
|
||||
"type": "image/png"} for n in (192, 256, 512)]}
|
||||
return self._send(200, json.dumps(man).encode(), "application/manifest+json")
|
||||
if self.path.split("?")[0] == "/favicon.ico":
|
||||
with open(os.path.join(HERE, "web", "ps5directo.ico"), "rb") as f:
|
||||
return self._send(200, f.read(), "image/x-icon")
|
||||
if self.path in ("/", "/index.html") or self.path.startswith("/?"):
|
||||
@@ -2581,7 +2595,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
d["has_password"] = bool(j.get("password"))
|
||||
d["pcx"] = (j.get("info") or {}).get("mode") == "pcx"
|
||||
jobs.append(d)
|
||||
st = {"ok": True, "version": VERSION, "settings": dict(SETTINGS), "jobs": jobs,
|
||||
st = {"ok": True, "version": VERSION, "home": HERE, "settings": dict(SETTINGS), "jobs": jobs,
|
||||
"inbox": len(INBOX), "repo": SETTINGS.get("update_repo") or DEFAULT_REPO}
|
||||
st["settings"].pop("ftp_pass", None)
|
||||
return self._send(200, st)
|
||||
@@ -2879,10 +2893,48 @@ def locate_files(files, budget=12.0, max_depth=6):
|
||||
return found
|
||||
|
||||
|
||||
class Server(ThreadingHTTPServer):
|
||||
"""En Windows, SO_REUSEADDR deja que DOS programas escuchen en el mismo puerto y las
|
||||
peticiones se reparten al azar entre los dos ("Failed to fetch", la cola que aparece y
|
||||
desaparece). Aqui el puerto es exclusivo."""
|
||||
allow_reuse_address = os.name != "nt"
|
||||
daemon_threads = True
|
||||
|
||||
def server_bind(self):
|
||||
if os.name == "nt" and hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
|
||||
super().server_bind()
|
||||
|
||||
|
||||
LOCK_PORT = 8776
|
||||
_instance_lock = []
|
||||
|
||||
|
||||
def take_instance_lock(wait=0.0):
|
||||
"""Solo un PS5 Directo a la vez: se queda con un puerto fijo mientras funciona.
|
||||
Devuelve False si otro ya lo tiene (tras esperar hasta `wait` segundos)."""
|
||||
end = time.time() + wait
|
||||
while True:
|
||||
s = socket.socket()
|
||||
try:
|
||||
if os.name == "nt" and hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
|
||||
s.bind(("127.0.0.1", LOCK_PORT))
|
||||
_instance_lock.append(s)
|
||||
return True
|
||||
except OSError:
|
||||
s.close()
|
||||
if time.time() >= end:
|
||||
return False
|
||||
time.sleep(0.25)
|
||||
|
||||
|
||||
def pick_port():
|
||||
for port in (8777, 8778, 8779, 0):
|
||||
s = socket.socket()
|
||||
try:
|
||||
if os.name == "nt" and hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
|
||||
s.bind(("127.0.0.1", port))
|
||||
p = s.getsockname()[1]
|
||||
s.close()
|
||||
@@ -2907,20 +2959,45 @@ def dropped_paths():
|
||||
return out
|
||||
|
||||
|
||||
def running_session():
|
||||
"""Devuelve (url_base, token) si ya hay un PS5 Directo funcionando."""
|
||||
def running_session(full=False):
|
||||
"""Devuelve (url_base, token) si ya hay un PS5 Directo funcionando
|
||||
(con full=True, (url_base, token, estado))."""
|
||||
try:
|
||||
with open(SESSION_FILE, encoding="utf-8") as f:
|
||||
ses = json.load(f)
|
||||
base = "http://127.0.0.1:%d" % int(ses["port"])
|
||||
req = urllib.request.Request(base + "/api/state", headers={"X-Token": ses["token"]})
|
||||
with urllib.request.urlopen(req, timeout=3) as r:
|
||||
json.load(r)
|
||||
return base, ses["token"]
|
||||
st = json.load(r)
|
||||
return (base, ses["token"], st) if full else (base, ses["token"])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def replace_other_version(ses):
|
||||
"""Si lo que ya esta abierto es OTRA version (o la de otra carpeta, por ejemplo una copia
|
||||
vieja del escritorio) y no tiene nada en marcha, la cierra para abrir esta.
|
||||
Devuelve True si se cerro."""
|
||||
base, token, st = ses
|
||||
if st.get("version") == VERSION and st.get("home", HERE) == HERE:
|
||||
return False
|
||||
for j in st.get("jobs") or []:
|
||||
if j.get("status") not in ("hecho", "error", "cancelado", "pausado"):
|
||||
return False # esta trabajando: no se le corta
|
||||
try:
|
||||
req = urllib.request.Request(base + "/api/quit", data=b"{}",
|
||||
headers={"X-Token": token, "Content-Type": "application/json"})
|
||||
urllib.request.urlopen(req, timeout=5).read()
|
||||
except Exception:
|
||||
pass
|
||||
for _ in range(40):
|
||||
time.sleep(0.25)
|
||||
if not running_session():
|
||||
print("Cerrada la version %s que seguia abierta" % st.get("version", "antigua"))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def forward_to_running(ses, paths):
|
||||
base, token = ses
|
||||
try:
|
||||
@@ -2965,6 +3042,70 @@ def open_window(url):
|
||||
return None
|
||||
|
||||
|
||||
def _powershell(script, timeout=30):
|
||||
import subprocess
|
||||
return subprocess.run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
|
||||
|
||||
def ensure_shortcuts():
|
||||
"""Acceso directo "PS5 Directo" en el escritorio y en "Enviar a", con el icono del programa.
|
||||
Se crea la primera vez; despues solo se corrige si apunta a otra carpeta (una copia vieja)."""
|
||||
if os.name != "nt":
|
||||
return
|
||||
try:
|
||||
vbs = os.path.join(HERE, "PS5 Directo.vbs")
|
||||
ico = os.path.join(HERE, "web", "ps5directo.ico")
|
||||
if not os.path.isfile(vbs):
|
||||
return
|
||||
with LOCK:
|
||||
first = not SETTINGS.get("shortcut_done")
|
||||
q = lambda t: "'" + t.replace("'", "''") + "'"
|
||||
script = (
|
||||
"$w=New-Object -ComObject WScript.Shell; $first=$%s;"
|
||||
"foreach($f in @([Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('SendTo'))){"
|
||||
" $p=Join-Path $f 'PS5 Directo.lnk'; $ex=Test-Path -LiteralPath $p;"
|
||||
" $l=$w.CreateShortcut($p);"
|
||||
" $want='\"'+%s+'\"';"
|
||||
" if($ex -and $l.Arguments -eq $want -and $l.IconLocation -like ('*'+%s+'*')){continue}"
|
||||
" if(-not $ex -and -not $first){continue}"
|
||||
" if($ex -and $l.Arguments -notlike '*PS5 Directo.vbs*'){continue}"
|
||||
" $l.TargetPath=(Join-Path $env:WINDIR 'System32\\wscript.exe'); $l.Arguments=$want;"
|
||||
" $l.WorkingDirectory=%s; $l.IconLocation=%s+',0'; $l.Description='PS5 Directo'; $l.Save();"
|
||||
" 'ok '+$p }"
|
||||
) % ("true" if first else "false", q(vbs), q(ico), q(HERE), q(ico))
|
||||
r = _powershell(script)
|
||||
if r.stdout.strip():
|
||||
print("Acceso directo:", r.stdout.strip().replace("\n", " | "))
|
||||
if r.returncode == 0 and first:
|
||||
with LOCK:
|
||||
SETTINGS["shortcut_done"] = True
|
||||
save_settings()
|
||||
except Exception as e:
|
||||
print("No se pudo crear el acceso directo:", e)
|
||||
|
||||
|
||||
def refresh_window_icon():
|
||||
"""La ventana guarda el icono en su cache: tras actualizar se borra para que la barra
|
||||
de tareas enseñe el icono actual y no el de una version vieja."""
|
||||
with LOCK:
|
||||
if SETTINGS.get("icon_ver") == VERSION:
|
||||
return
|
||||
SETTINGS["icon_ver"] = VERSION
|
||||
save_settings()
|
||||
prof = os.path.join(DATA_DIR, "ventana")
|
||||
for sub in ("Default", ""):
|
||||
for name in ("Favicons", "Favicons-journal"):
|
||||
try:
|
||||
os.remove(os.path.join(prof, sub, name))
|
||||
except OSError:
|
||||
pass
|
||||
import shutil
|
||||
for d in ("Web Applications", os.path.join("Default", "Web Applications")):
|
||||
shutil.rmtree(os.path.join(prof, d), ignore_errors=True)
|
||||
|
||||
|
||||
def busy():
|
||||
with LOCK:
|
||||
return any(j["status"] not in ("hecho", "error", "cancelado", "pausado") for j in JOBS)
|
||||
@@ -2997,8 +3138,21 @@ def main():
|
||||
if not running_session():
|
||||
break
|
||||
time.sleep(0.5)
|
||||
ses = running_session()
|
||||
ses = running_session(full=True)
|
||||
if ses and replace_other_version(ses):
|
||||
ses = None
|
||||
if not ses and not take_instance_lock(wait=30 if "--after-update" in sys.argv else 4):
|
||||
# otro PS5 Directo esta arrancando ahora mismo (doble clic, por ejemplo): uso ese
|
||||
for _ in range(40):
|
||||
ses = running_session(full=True)
|
||||
if ses:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
if not ses:
|
||||
print("Ya hay otro PS5 Directo abierto")
|
||||
sys.exit(3)
|
||||
if ses:
|
||||
ses = ses[:2]
|
||||
if paths:
|
||||
forward_to_running(ses, paths)
|
||||
print("Anadido a la cola de PS5 Directo (ya estaba abierto)")
|
||||
@@ -3014,13 +3168,15 @@ def main():
|
||||
for target in (transfer_worker, extract_worker):
|
||||
threading.Thread(target=target, daemon=True).start()
|
||||
port = pick_port()
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
||||
srv = Server(("127.0.0.1", port), Handler)
|
||||
try:
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
with open(SESSION_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump({"port": srv.server_address[1], "token": TOKEN}, f)
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=ensure_shortcuts, daemon=True).start()
|
||||
refresh_window_icon()
|
||||
url = "http://127.0.0.1:%d/#t=%s" % (srv.server_address[1], TOKEN)
|
||||
print("PS5 Directo %s" % VERSION)
|
||||
print("Abre en el navegador: %s" % url)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
+20
-4
@@ -4,7 +4,10 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PS5 Directo</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.ico?v=1.18.2" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="/icon-256.png?v=1.18.2">
|
||||
<link rel="apple-touch-icon" href="/icon-192.png?v=1.18.2">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0e1116;--panel:#161b22;--panel2:#1c232c;--line:#2a323d;--text:#e6edf3;--muted:#8b97a5;
|
||||
@@ -404,7 +407,7 @@ tr:hover .rowact{opacity:1}
|
||||
<div class="toast" id="toast"></div>
|
||||
<script>
|
||||
const I18N_EXACT={"Novedades": "What's new", "Entendido": "Got it", "Se ha actualizado. Esto es lo nuevo:": "PS5 Directo has been updated. Here is what's new:", "Transferencias es la cola. En Archivos de la consola navegas por la PS5: ves tamaños, copias, mueves, renombras, borras, bajas al PC o subes archivos y carpetas. Espacio te enseña en un gráfico qué ocupa más y qué puedes borrar. Actividad guarda el historial.": "Transfers is the queue. In Console files you browse the PS5: see sizes, copy, move, rename, delete, download to the PC or upload files and folders. Space shows in a chart what takes up the most room and what you can delete. Activity keeps the history.", "Espacio": "Space", "Almacenamiento interno": "Internal storage", "Analizar": "Analyze", "Mira qué ocupa sitio en la consola, con nombres y tamaños, para decidir qué borrar. Avisa de juegos repetidos (por ejemplo la carpeta y su versión convertida), envíos a medias y restos. Necesita BFpilot.": "See what takes up space on the console, with names and sizes, to decide what to delete. It flags repeated games (for example a folder and its converted version), half-sent transfers and leftovers. Needs BFpilot.", "Todo lo que ocupa sitio": "Everything that takes up space", "Borrar seleccionados": "Delete selected", "Disco externo": "External drive", "Libre": "Free", "Abrir": "Open", "Borrando…": "Deleting…", "¿Borrar de la consola? No se puede deshacer.": "Delete from the console? This cannot be undone.", "Juegos instalados desde la PS5": "Games installed from the PS5", "Sistema y otros datos de la consola": "System and other console data", "Otros": "Other", "Borrado. Vuelvo a medir el espacio…": "Deleted. Measuring the space again…", "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 copiLine truncated
|
||||
const I18N_RX=[["^Este ZIP usa cifrado AES y BFpilot todavia no sabe abrirlo \\(solo el cifrado ZIP clasico\\)\\. Los archivos ya estan en la PS5, en ([\\s\\S]*?)\\. Puedes bajarlos al PC, extraerlos con 7-Zip y subir la carpeta\\.$", "This ZIP uses AES encryption and BFpilot can't open it yet (only classic ZIP encryption). The files are already on the PS5, in {0}. You can download them to the PC, extract them with 7-Zip and upload the folder."], ["^Aviso: lo descomprimido puede ocupar hasta ~5 veces el RAR \\(~([\\s\\S]*?)\\) y quedan ([\\s\\S]*?) libres\\. Puede bastar, pero si el espacio baja del margen lo paro\\. Si puedes, libera espacio\\.$", "Warning: extracted content can take up to ~5 times the RAR (~{0}) and {1} is free. It may be enough, but if space drops below the margin I stop. Free up space if you can."], ["^Ojo: el contenido descomprimido puede ocupar hasta ~5 veces el RAR \\(~([\\s\\S]*?)\\) y no se si cabra\\. Si al descomprimir baja del margen, lo paro y los RAR se quedan en la PS5\\.$", "Heads up: extracted content can take up to ~5 times the RAR (~{0}) and it may not fit. If space drops below the margin while extracting, I stop and the RARs stay on the PS5."], ["^AVISO: no pude poner los permisos de ([\\s\\S]*?) \\(([\\s\\S]*?)\\)\\. Si no arranca, en FileZilla: clic derecho en la carpeta > Permisos > 777 > recurrir a subdirectorios\\.$", "WARNING: couldn't set permissions on {0} ({1}). If it doesn't launch, in FileZilla: right-click the folder > File permissions > 777 > recurse into subdirectories."], ["^No cabe en el PC: hay que bajar ([\\s\\S]*?) a ([\\s\\S]*?) y solo hay ([\\s\\S]*?) libres\\. Libera espacio, cambia la carpeta temporal en Ajustes o descomprime en la PS5\\.$", "Not enough space on the PC: {0} must be downloaded to {1} and only {2} is free. Free up space, change the temporary folder in Settings, or extract on the PS5."], ["^No cabe en la PS5: descomprimido ocupa ([\\s\\S]*?), faltan por mandar ([\\s\\S]*?), hay ([\\s\\S]*?) libres y dejo ([\\s\\S]*?) de margen\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: extracted it takes {0}, {1} left to send, {2} free and I keep {3} as margin. Free up space and click Retry."], ["^No hay sitio en la PS5: faltan por enviar ([\\s\\S]*?), hay ([\\s\\S]*?) libres y dejo ([\\s\\S]*?) de margen \\(Ajustes\\)\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: {0} left to send, {1} free and I keep {2} as margin (Settings). Free up space and click Retry."], ["^La IP de la PS5 no es valida \\(([\\s\\S]*?)\\): en Ajustes escribe solo los numeros, por ejemplo 192\\.168\\.1\\.50, sin ftp:\\/\\/ ni puerto$", "The PS5 IP is not valid ({0}): in Settings type just the numbers, for example 192.168.1.50, without ftp:// or a port"], ["^Parado para no llenar la PS5 \\(([\\s\\S]*?)\\): quedan ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar; sigue donde iba\\.$", "Stopped so the PS5 doesn't fill up ({0}): {1} free and the margin is {2}. Free up space and click Retry; it continues where it left off."], ["^BFpilot no abre ZIP partidos \\(\\.z01, \\.z02\\.\\.\\.\\)\\. Uno los RAR\\/7z partidos, pero este formato no\\. Los archivos estan en ([\\s\\S]*?)\\.$", "BFpilot can't open split ZIPs (.z01, .z02...). I can join split RAR/7z, but not this format. The files are in {0}."], ["^No cabe en la PS5: faltan ([\\s\\S]*?) por enviar, hay ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar\\.$", "It doesn't fit on the PS5: {0} left to send, {1} free and the margin is {2}. Free up space and click Retry."], ["^Ojo: ya tienes ([\\s\\S]*?) en ([\\s\\S]*?)\\. Dos copias con el mismo Title ID pueden liar a ShadowMountPlus \\(deja solo una\\)\\.([\\s\\S]*?)$", "Heads up: you already have {0} in {1}. Two copies with the same Title ID can confuse ShadowMountPlus (keep only one).{2}"], ["^Ojo: ya tienes ([\\s\\S]*?) en ([\\s\\S]*?)\\. Dos copias con el mismo Title ID pueden liar a ShadowMountPlus \\(deja solo una\\)\\.$", "Heads up: you already have {0} in {1}. Two copies with the same Title ID can confuse ShadowMountPlus (keep only one)."], ["^El comprimido tiene enlaces \\(([\\s\\S]*?)\\); eso solo lo sabe extraer BFpilot\\. Descomprimelo en la PS5\\.$", "The archive contains links ({0}); only BFpilot can extract those. Extract it on the PS5."], ["^No hay sitio en la PS5: quedan ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: {0} free and the margin is {1}. Free up space and click Retry."], ["^Ya existe ([\\s\\S]*?)\\. No lo piso: borralo o renombralo y pulsa Reintentar\\. Lo extraido esta en ([\\s\\S]*?)$", "{0} already exists. I won't overwrite it: delete or rename it and click Retry. What was extracted is in {1}"], ["^Recibido de la extensión: ([\\s\\S]*?)\\. Cuando estén todas las partes, pulsa \"Añadir a la cola\"\\.$", "Received from the extension: {0}. When all parts are iLine truncated
|
||||
const I18N_RX=[["^Este ZIP usa cifrado AES y BFpilot todavia no sabe abrirlo \\(solo el cifrado ZIP clasico\\)\\. Los archivos ya estan en la PS5, en ([\\s\\S]*?)\\. Puedes bajarlos al PC, extraerlos con 7-Zip y subir la carpeta\\.$", "This ZIP uses AES encryption and BFpilot can't open it yet (only classic ZIP encryption). The files are already on the PS5, in {0}. You can download them to the PC, extract them with 7-Zip and upload the folder."], ["^Aviso: lo descomprimido puede ocupar hasta ~5 veces el RAR \\(~([\\s\\S]*?)\\) y quedan ([\\s\\S]*?) libres\\. Puede bastar, pero si el espacio baja del margen lo paro\\. Si puedes, libera espacio\\.$", "Warning: extracted content can take up to ~5 times the RAR (~{0}) and {1} is free. It may be enough, but if space drops below the margin I stop. Free up space if you can."], ["^Ojo: el contenido descomprimido puede ocupar hasta ~5 veces el RAR \\(~([\\s\\S]*?)\\) y no se si cabra\\. Si al descomprimir baja del margen, lo paro y los RAR se quedan en la PS5\\.$", "Heads up: extracted content can take up to ~5 times the RAR (~{0}) and it may not fit. If space drops below the margin while extracting, I stop and the RARs stay on the PS5."], ["^AVISO: no pude poner los permisos de ([\\s\\S]*?) \\(([\\s\\S]*?)\\)\\. Si no arranca, en FileZilla: clic derecho en la carpeta > Permisos > 777 > recurrir a subdirectorios\\.$", "WARNING: couldn't set permissions on {0} ({1}). If it doesn't launch, in FileZilla: right-click the folder > File permissions > 777 > recurse into subdirectories."], ["^No cabe en el PC: hay que bajar ([\\s\\S]*?) a ([\\s\\S]*?) y solo hay ([\\s\\S]*?) libres\\. Libera espacio, cambia la carpeta temporal en Ajustes o descomprime en la PS5\\.$", "Not enough space on the PC: {0} must be downloaded to {1} and only {2} is free. Free up space, change the temporary folder in Settings, or extract on the PS5."], ["^No cabe en la PS5: descomprimido ocupa ([\\s\\S]*?), faltan por mandar ([\\s\\S]*?), hay ([\\s\\S]*?) libres y dejo ([\\s\\S]*?) de margen\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: extracted it takes {0}, {1} left to send, {2} free and I keep {3} as margin. Free up space and click Retry."], ["^No hay sitio en la PS5: faltan por enviar ([\\s\\S]*?), hay ([\\s\\S]*?) libres y dejo ([\\s\\S]*?) de margen \\(Ajustes\\)\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: {0} left to send, {1} free and I keep {2} as margin (Settings). Free up space and click Retry."], ["^La IP de la PS5 no es valida \\(([\\s\\S]*?)\\): en Ajustes escribe solo los numeros, por ejemplo 192\\.168\\.1\\.50, sin ftp:\\/\\/ ni puerto$", "The PS5 IP is not valid ({0}): in Settings type just the numbers, for example 192.168.1.50, without ftp:// or a port"], ["^Parado para no llenar la PS5 \\(([\\s\\S]*?)\\): quedan ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar; sigue donde iba\\.$", "Stopped so the PS5 doesn't fill up ({0}): {1} free and the margin is {2}. Free up space and click Retry; it continues where it left off."], ["^BFpilot no abre ZIP partidos \\(\\.z01, \\.z02\\.\\.\\.\\)\\. Uno los RAR\\/7z partidos, pero este formato no\\. Los archivos estan en ([\\s\\S]*?)\\.$", "BFpilot can't open split ZIPs (.z01, .z02...). I can join split RAR/7z, but not this format. The files are in {0}."], ["^No cabe en la PS5: faltan ([\\s\\S]*?) por enviar, hay ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar\\.$", "It doesn't fit on the PS5: {0} left to send, {1} free and the margin is {2}. Free up space and click Retry."], ["^Ojo: ya tienes ([\\s\\S]*?) en ([\\s\\S]*?)\\. Dos copias con el mismo Title ID pueden liar a ShadowMountPlus \\(deja solo una\\)\\.([\\s\\S]*?)$", "Heads up: you already have {0} in {1}. Two copies with the same Title ID can confuse ShadowMountPlus (keep only one).{2}"], ["^Ojo: ya tienes ([\\s\\S]*?) en ([\\s\\S]*?)\\. Dos copias con el mismo Title ID pueden liar a ShadowMountPlus \\(deja solo una\\)\\.$", "Heads up: you already have {0} in {1}. Two copies with the same Title ID can confuse ShadowMountPlus (keep only one)."], ["^El comprimido tiene enlaces \\(([\\s\\S]*?)\\); eso solo lo sabe extraer BFpilot\\. Descomprimelo en la PS5\\.$", "The archive contains links ({0}); only BFpilot can extract those. Extract it on the PS5."], ["^No hay sitio en la PS5: quedan ([\\s\\S]*?) libres y el margen es ([\\s\\S]*?)\\. Libera espacio y pulsa Reintentar\\.$", "Not enough space on the PS5: {0} free and the margin is {1}. Free up space and click Retry."], ["^Ya existe ([\\s\\S]*?)\\. No lo piso: borralo o renombralo y pulsa Reintentar\\. Lo extraido esta en ([\\s\\S]*?)$", "{0} already exists. I won't overwrite it: delete or rename it and click Retry. What was extracted is in {1}"], ["^Recibido de la extensión: ([\\s\\S]*?)\\. Cuando estén todas las partes, pulsa \"Añadir a la cola\"\\.$", "Received from the extension: {0}. When all parts are iLine truncated
|
||||
// --- 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'})();
|
||||
@@ -469,7 +472,18 @@ 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)}
|
||||
// Consultas que se pueden repetir sin efectos: si falla la conexion un instante, se reintenta
|
||||
const SAFE_API=new Set(['/api/state','/api/test','/api/settings','/api/places','/api/whatsnew','/api/space/state']);
|
||||
async function api(path, body){
|
||||
for(let i=0;;i++){
|
||||
try{ return await api1(path, body) }
|
||||
catch(e){
|
||||
if(!(e instanceof TypeError) || i>=2 || !(body===undefined || SAFE_API.has(path))) throw e;
|
||||
await new Promise(r=>setTimeout(r, 400*(i+1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
async function api1(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));
|
||||
@@ -597,9 +611,11 @@ function paintConn(r){
|
||||
}
|
||||
async function checkConn(){try{paintConn(await api('/api/test',{}))}catch(e){}}
|
||||
|
||||
let refreshFails=0, refreshing=false;
|
||||
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>`}
|
||||
if(refreshing) return; refreshing=true;
|
||||
try{const s=await api('/api/state');refreshFails=0;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){ if(++refreshFails<5) return; $('jobs').innerHTML=`<div class="empty">No conecto con el programa. Cierra esta ventana y abre PS5 Directo otra vez. (${esc(e.message)})</div>`}
|
||||
}
|
||||
$('dest').addEventListener('input',()=>$('dest').dataset.touched='1');
|
||||
$('where').addEventListener('change',()=>$('where').dataset.touched='1');
|
||||
|
||||
Reference in new issue
Block a user