diff --git a/LEEME.txt b/LEEME.txt index 8dec95d..cf04d7f 100644 --- a/LEEME.txt +++ b/LEEME.txt @@ -1,4 +1,4 @@ -PS5 DIRECTO 1.16 +PS5 DIRECTO 1.17 ================ (English: see README_EN.txt) @@ -162,6 +162,15 @@ de quien es la culpa cuando va lento. Tambien hay uno solo PC -> PS5 en Ajustes. +NOVEDADES 1.17 +- Enlaces ftp:// (un NAS u otro servidor): ftp://usuario:clave@nas/ruta/archivo.rar + Funcionan como los enlaces normales, con reanudación. +- Comprimidos .tar, .tar.gz, .tar.bz2 y .tar.xz: se descomprimen en el PC + sobre la marcha (no hace falta 7-Zip ni BFpilot). +- Las carpetas se envían con varias conexiones a la vez (Ajustes > Partes a + la vez), más rápido con muchos archivos. +- Pasos propios para los envíos de carpetas: Comparar → Enviar → Listo. + NOVEDADES 1.16 - Pestaña "Archivos de la consola": explorador con tamaños, copiar, mover, renombrar, borrar, bajar al PC, permisos y subir aquí. @@ -278,7 +287,8 @@ SI ALGO FALLA ENLACES ------- -Funcionan los enlaces de descarga directa. Pixeldrain, Dropbox y algunos +Funcionan los enlaces de descarga directa, y también los ftp:// de un NAS +u otro servidor (ftp://usuario:clave@nas/ruta/archivo.rar). 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. diff --git a/README.md b/README.md index e366007..8e26cc0 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ A Windows file-transfer tool for jailbroken PS5 consoles. It sends files to the console over FTP **while they download**, without storing them on your PC. It then extracts them, either on the console with BFpilot or on the PC with 7-Zip, and places apps in `/data/homebrew` so ShadowMountPlus shows them in the menu. - ✅ Streams downloads straight to the console: nothing is saved on the PC -- ✅ Joins split parts (.part1, .part2… / .001, .002…) and extracts RAR, 7z and ZIP +- ✅ Works from download links, ftp:// links (e.g. a NAS) or files on your PC +- ✅ Joins split parts (.part1, .part2… / .001, .002…) and extracts RAR, 7z, ZIP and tar (.gz/.bz2/.xz) - ✅ Extracts on the console (BFpilot) or on the PC (7-Zip, every file CRC-checked) - ✅ Fixes the file permissions that stop BFpilot-extracted content from launching (CE-107750-0) - ✅ Pause, resume and retry where it left off, even after a dropped connection - ✅ Built-in console file browser: sizes, copy, move, rename, delete, download to PC, upload -- ✅ Send whole PC folders, skipping what's already there and resuming what got cut off +- ✅ Send whole PC folders over several connections, skipping what's already there and resuming what got cut off - ✅ Watches free space so the console never fills up - ✅ Speed limit and built-in speed test - ✅ Optional browser extension: links arrive by themselves, no copy and paste diff --git a/README_EN.txt b/README_EN.txt index c2acda0..49c6f7d 100644 --- a/README_EN.txt +++ b/README_EN.txt @@ -1,4 +1,4 @@ -PS5 DIRECTO 1.16 — USER GUIDE (English) +PS5 DIRECTO 1.17 — USER GUIDE (English) ======================================= Paste links (or drag files from your PC) and the tool does the rest: @@ -201,7 +201,8 @@ IF SOMETHING GOES WRONG LINKS ----- -Direct download links work. Pixeldrain, Dropbox and some Mediafire and +Direct download links work, and so do ftp:// links to a NAS or another +server (ftp://user:password@nas/path/file.rar). Pixeldrain, Dropbox and some Mediafire and Google Drive links are converted automatically. Pages that require a captcha or a login don't work directly: copy the final download link, or use the browser extension. @@ -214,6 +215,9 @@ Settings and queue are stored in %USERPROFILE%\.ps5directo WHAT'S NEW ---------- +1.17 ftp:// links (a NAS or other server) with resume. .tar/.tar.gz/.tar.bz2/ + .tar.xz extracted on the PC (no 7-Zip needed). Folders upload over several + connections at once. Own steps for folder uploads. 1.16 "Console files" tab (file browser: sizes, copy, move, rename, delete, download to PC, permissions, upload here). Send PC folders with resume. "Choose files…/Choose folder…" buttons. "Activity" tab. diff --git a/app.py b/app.py index ce29f1f..ec1e790 100644 --- a/app.py +++ b/app.py @@ -21,7 +21,7 @@ HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import engine as E # noqa: E402 -VERSION = "1.16.0" +VERSION = "1.17.0" # 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" @@ -397,7 +397,7 @@ def transfer_job(job, srcs, cancel): return folder_job(job, srcs[0], cancel) names = [s.name for s in srcs] archives = [s for s in srcs if E.is_archive(s.name)] - if archives and job_where(job) == "pc": + if archives and (job_where(job) == "pc" or any(E.is_tar(a.name) for a in archives)): return pc_extract_job(job, srcs, cancel) m = E.SPLIT_RAW_RE.match(srcs[0].name) raw_split = (not archives and len(srcs) >= 1 and m @@ -530,8 +530,10 @@ def raw_job(job, srcs, cancel): target = dest + "/" + s.name if s.kind == "folder": setj(job, phase="Enviando carpeta: %s" % s.name) - E.upload_tree(cfg, s, target, cancel, lambda m: jlog(job, m), - lambda v, rel: prog(v)) + sent, skipped, resumed = E.upload_tree(cfg, s, target, cancel, lambda m: jlog(job, m), + lambda v, rel: prog(v)) + jlog(job, "Carpeta enviada: %d archivos nuevos, %d ya estaban iguales, %d continuados" + % (sent, skipped, resumed)) else: setj(job, phase="Enviando %s" % s.name) E.upload(cfg, target, s.size, lambda off, s=s: s.stream(off, cfg, cancel, meter), @@ -1072,11 +1074,12 @@ def pc_extract_job(job, srcs, cancel): cfg = cfg_snapshot() cfg["_limit_bps"] = live_limit_bps log = lambda m: jlog(job, m) # noqa: E731 - exe = E.find_7z(cfg.get("sevenzip_path")) - if not exe: + names = [s.name for s in srcs] + tar = any(E.is_tar(n) for n in names) + exe = None if tar else E.find_7z(cfg.get("sevenzip_path")) + if not tar and not exe: raise E.PipeError("Para descomprimir en el PC hace falta 7-Zip (gratis, 7-zip.org). Instalalo, " "o pon la ruta de 7z.exe en Ajustes, o elige 'Descomprimir en la PS5'.") - names = [s.name for s in srcs] folder = os.path.join(pc_temp_root(cfg), E.safe_name(E.set_key(srcs[0].name))) dl, up = Meter(), Meter() phase = {"dl": True} @@ -1098,6 +1101,9 @@ def pc_extract_job(job, srcs, cancel): speed_label="Descarga al PC", sent_label="Bajado al PC", pc_step="bajar") local, created = fetch_to_pc(job, srcs, cfg, cancel, dl, folder) first = local[E.first_volume(names)] + if tar: + phase["dl"] = False + return pc_tar_job(job, srcs, cfg, cancel, local[names[0]], created, folder, up) # 2) Leer el indice (con contrasena si hace falta) setj(job, phase="Leyendo el contenido del comprimido", speed_label="", sent_label="", @@ -1292,6 +1298,60 @@ def ftp_place(job, info, dest, cfg): pass +def pc_tar_job(job, srcs, cfg, cancel, path, created, folder, up): + """.tar / .tar.gz / .tar.bz2 / .tar.xz: se descomprime en el PC con Python (sin 7-Zip).""" + staging = "%s/extract/%s" % (WORK_ROOT, job["id"]) + total = os.path.getsize(path) + sig = "tar|%s|%d" % (os.path.basename(path), total) + st = _pcx_state(job) + skip = int(st.get("done") or 0) if st.get("sig") == sig else 0 + bf = E.BFpilot(cfg) + if skip: + try: + if not bf.alive() or not bf.exists(staging): + skip = 0 + except Exception: + skip = 0 + free = free_space(bf) + if free is not None: + jlog(job, "Espacio libre en la PS5: %s" % human(free)) + if free < margin_bytes(): + raise E.PipeError("No hay sitio en la PS5: quedan %s libres y el margen es %s. Libera espacio " + "y pulsa Reintentar." % (human(free), human(margin_bytes()))) + jlog(job, "Descomprimiendo el .tar en el PC y enviando a la PS5") + setj(job, status="enviando", total=total, sent=0, pc_step="enviar", sent_label="Leído del comprimido", + speed_label="Velocidad (comprimido)", phase="Descomprimiendo en el PC y enviando a la PS5") + last = {"v": 0, "t": 0.0} + + def progress(v, tot, name): + if v > last["v"]: + up(v - last["v"]) + last["v"] = v + now = time.time() + if now - last["t"] > 0.5: + last["t"] = now + setj(job, sent=v, phase="Descomprimiendo en el PC y enviando: %s" % name.rsplit("/", 1)[-1][-60:]) + + def on_file(n): + _pcx_save(job, {"sig": sig, "done": n}) + watch = start_space_watch(job, bf, cancel, "el envio") + try: + nfiles, nbytes, params = E.stream_extract_tar(path, staging, cfg, cancel, lambda m: jlog(job, m), + progress, on_file, skip) + finally: + watch.set() + setj(job, sent=total) + jlog(job, "Enviado ya descomprimido: %d archivos, %s" % (nfiles, human(nbytes))) + games = [] + for pj, raw in params.items(): + root = pj[:-len("sce_sys/param.json")].rstrip("/") + if any(root == g["root"] or root.startswith(g["root"] + "/") for g in games if g["root"]): + continue + games.append({"root": root, "tid": E.title_id(raw.decode("utf-8", "replace")) or E.title_id(root)}) + return {"mode": "pcx", "staging": staging, "hint": E.display_name(srcs[0].name), "games": games, + "pc_files": created, "pc_folder": folder if created else ""} + + def finish_pc_extract(job, info, bf, dest, cancel, cfg): staging = info["staging"] setj(job, status="colocando", phase="Colocando en su sitio") diff --git a/bg.js b/bg.js new file mode 100644 index 0000000..045c53b --- /dev/null +++ b/bg.js @@ -0,0 +1,71 @@ +const EN = !(navigator.language || "es").toLowerCase().startsWith("es"); +// PS5 Directo - captura de enlaces. +// Cuando el navegador empieza a descargar un archivo comprimido o paquete (.rar, .7z, .zip, .pkg...), +// manda el enlace FINAL (despues de redirecciones) a PS5 Directo y cancela la descarga. +// Si PS5 Directo no esta abierto, no toca nada: la descarga sigue normal. + +const WANTED_FILE = /\.(part\d+\.rar|rar|r\d{2,3}|7z(\.\d{3})?|zip|z\d{2}|\d{3}|pkg|exfat|ffpkg|ffpfsc?|ufs)$/i; +const PORTS = [8777, 8778, 8779]; + +async function settings() { + return chrome.storage.local.get({ enabled: true, captured: 0 }); +} + +async function findApp() { + for (const port of PORTS) { + try { + const r = await fetch(`http://127.0.0.1:${port}/api/ext/ping`, { cache: "no-store" }); + if (r.ok) { + const j = await r.json(); + if (j.app === "PS5 Directo") return { port, version: j.version }; + } + } catch (e) { /* no esta en ese puerto */ } + } + return null; +} + +function fileName(item) { + const f = (item.filename || "").split(/[\\/]/).pop(); + if (f) return f; + try { return decodeURIComponent(new URL(item.finalUrl || item.url).pathname.split("/").pop()); } + catch (e) { return ""; } +} + +async function handle(item) { + const cfg = await settings(); + if (!cfg.enabled) return; + const url = item.finalUrl || item.url; + if (!/^https?:/i.test(url)) return; + const name = fileName(item); + const path = url.split("?")[0]; + if (!WANTED_FILE.test(name) && !WANTED_FILE.test(path)) return; // no es un archivo que nos interese + const app = await findApp(); + if (!app) { // PS5 Directo cerrado: no tocar + chrome.action.setBadgeText({ text: "off" }); + return; + } + try { + const r = await fetch(`http://127.0.0.1:${app.port}/api/ext/capture`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url, original: item.url, name }) + }); + if (!r.ok) return; + } catch (e) { return; } + try { await chrome.downloads.cancel(item.id); } catch (e) {} + try { await chrome.downloads.erase({ id: item.id }); } catch (e) {} + const n = (cfg.captured || 0) + 1; + await chrome.storage.local.set({ captured: n, last: name }); + chrome.action.setBadgeBackgroundColor({ color: "#2563eb" }); + chrome.action.setBadgeText({ text: String(n % 1000) }); + chrome.notifications.create({ + type: "basic", iconUrl: "icon128.png", title: EN ? "Sent to PS5 Directo" : "Enviado a PS5 Directo", + message: (name || (EN ? "Link" : "Enlace")) + (EN ? " — not downloaded to the PC." : " — no se descarga en el PC.") + }); +} + +// Cuando ya se sabe el nombre del archivo y el enlace final (tras las redirecciones) +chrome.downloads.onDeterminingFilename.addListener((item, suggest) => { + suggest(); // no cambiamos el nombre + handle(item); +}); diff --git a/download b/download new file mode 100644 index 0000000..b3ddd3e --- /dev/null +++ b/download @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.backup/ +*.nuevo diff --git a/engine.py b/engine.py index 538e763..afe6844 100644 --- a/engine.py +++ b/engine.py @@ -13,6 +13,7 @@ import ftplib import json import math import os +import posixpath import re import socket import ssl @@ -40,7 +41,12 @@ class PipeError(Exception): # --------------------------------------------------------------------------- ARCHIVE_RE = re.compile( - r"\.(part\d+\.rar|rar|r\d{2,3}|7z|7z\.\d{3}|zip|z\d{2})$", re.I) + r"\.(part\d+\.rar|rar|r\d{2,3}|7z|7z\.\d{3}|zip|z\d{2}|tar|tar\.gz|tgz|tar\.bz2|tbz2?|tar\.xz|txz)$", re.I) +TAR_RE = re.compile(r"\.(tar|tar\.gz|tgz|tar\.bz2|tbz2?|tar\.xz|txz)$", re.I) + + +def is_tar(name): + return bool(TAR_RE.search(name or "")) SPLIT_RAW_RE = re.compile(r"^(?P.+)\.(?P\d{3})$") IMAGE_EXTS = (".exfat", ".ffpkg", ".ffpfs", ".ffpfsc", ".ufs") @@ -48,6 +54,7 @@ IMAGE_EXTS = (".exfat", ".ffpkg", ".ffpfs", ".ffpfsc", ".ufs") def display_name(name): """Nombre sin la parte de volumen (conserva mayusculas).""" for pat in (r"\.part\d+\.rar$", r"\.7z\.\d{3}$", r"\.r\d{2,3}$", r"\.z\d{2}$", + r"\.tar\.(gz|bz2|xz)$", r"\.(tgz|tbz2?|txz|tar)$", r"\.rar$", r"\.7z$", r"\.zip$", r"\.\d{3}$"): new = re.sub(pat, "", name, flags=re.I) if new != name: @@ -63,7 +70,8 @@ def set_key(name): """Nombre comun de un conjunto de volumenes (para agruparlos).""" n = name for pat in (r"\.part\d+\.rar$", r"\.7z\.\d{3}$", r"\.r\d{2,3}$", - r"\.z\d{2}$", r"\.rar$", r"\.7z$", r"\.zip$"): + r"\.z\d{2}$", r"\.tar\.(gz|bz2|xz)$", r"\.(tgz|tbz2?|txz|tar)$", + r"\.rar$", r"\.7z$", r"\.zip$"): new = re.sub(pat, "", n, flags=re.I) if new != n: return new.lower() @@ -529,10 +537,94 @@ class FolderSource: return self +class FtpSource: + """Un archivo en otro servidor FTP (un NAS, por ejemplo): ftp://usuario:clave@host:puerto/ruta""" + kind = "url" + + def __init__(self, url): + self.original = url.strip() + u = urllib.parse.urlparse(self.original) + self.host = u.hostname or "" + self.port = u.port or 21 + self.user = urllib.parse.unquote(u.username or "anonymous") + self.pw = urllib.parse.unquote(u.password or "") + self.path = urllib.parse.unquote(u.path or "/") + self.name = safe_name(posixpath.basename(self.path) or "archivo") + self.size = -1 + self.ranges = True + + def _connect(self): + f = ftplib.FTP() + f.encoding = "utf-8" + f.connect(self.host, self.port, timeout=30) + f.login(self.user, self.pw) + f.set_pasv(True) + f.voidcmd("TYPE I") + return f + + def probe(self): + try: + f = self._connect() + except Exception as e: + raise PipeError("No puedo conectar con el FTP %s:%s (%s)" % (self.host, self.port, e)) + try: + s = Ftp.size(f, self.path) + if s is None: + raise PipeError("No existe en el FTP: %s" % self.path) + self.size = s + finally: + try: + f.quit() + except Exception: + pass + return self + + def stream(self, offset, cfg, cancel, meter): + tries = 0 + while True: + f = None + try: + f = self._connect() + conn = f.transfercmd("RETR " + self.path, rest=offset or None) + with conn: + while True: + if cancel.is_set(): + raise Cancelled() + data = conn.recv(MiB) + if not data: + break + offset += len(data) + tries = 0 + meter(len(data)) + yield data + try: + f.voidresp() + except Exception: + pass + if self.size >= 0 and offset < self.size: + raise PipeError("el FTP corto la descarga") + return + except (Cancelled, GeneratorExit): + raise + except Exception as e: + tries += 1 + if tries > int(cfg.get("retries", 8)): + raise PipeError("Fallo al leer del FTP %s: %s" % (self.host, e)) + time.sleep(min(30, 2 * tries)) + finally: + if f is not None: + try: + f.close() + except Exception: + pass + + def make_source(text): text = text.strip().strip('"') if re.match(r"^https?://", text, re.I): return UrlSource(text) + if re.match(r"^ftp://", text, re.I): + return FtpSource(text) if os.path.isdir(text): return FolderSource(text) return FileSource(text) @@ -577,16 +669,21 @@ def _ftp_sizes(f, path): return out -def upload_tree(cfg, src, remote_root, cancel, log, progress): +def upload_tree(cfg, src, remote_root, cancel, log, progress, workers=None): """Manda una FolderSource a remote_root. Salta los archivos que ya estan con el mismo tamano, continua los que se cortaron (mas pequenos) y vuelve a mandar los demas. - progress(bytes_hechos, ruta_actual). Devuelve (enviados, saltados, continuados).""" + Usa varias conexiones a la vez (cfg parallel_files). progress(bytes_hechos, ruta_actual). + Devuelve (enviados, saltados, continuados).""" + import queue by_dir = {} for rel, size in src.files: d, _, n = rel.rpartition("/") by_dir.setdefault(d, []).append((n, size, rel)) - done = 0 - sent = skipped = resumed = 0 + workers = max(1, min(4, int(workers or cfg.get("parallel_files", 2) or 1))) + lock = threading.Lock() + st = {"done": 0, "sent": 0, "skipped": 0, "resumed": 0, "err": None} + tasks = queue.Queue() + # 1) una sola conexion: crear carpetas y ver que hay ya en la consola w = _FtpWriter(cfg, cancel, log) try: w.mkdir(remote_root) @@ -597,40 +694,73 @@ def upload_tree(cfg, src, remote_root, cancel, log, progress): w.mkdir(rdir) have = w._retry(lambda: ftp_sizes(w.f, rdir)) or {} for name, size, rel in by_dir[d]: - if cancel.is_set(): - raise Cancelled() - rpath = rdir + "/" + name got = have.get(name) if got == size: - skipped += 1 - done += size - progress(done, rel) + st["skipped"] += 1 + st["done"] += size continue offset = got if (got is not None and 0 < got < size) else 0 - if offset: - resumed += 1 - log("Continuando %s desde %s MB" % (rel, offset // MiB)) - local = os.path.join(src.path, *rel.split("/")) - base = done - w.begin(rpath, offset) - with open(local, "rb") as fh: - fh.seek(offset) - pos = offset - while True: - if cancel.is_set(): - raise Cancelled() - chunk = fh.read(4 * MiB) - if not chunk: - break - w.write(chunk) - pos += len(chunk) - progress(base + pos, rel) - w.end(size) - done = base + size - sent += 1 - return sent, skipped, resumed + tasks.put((rdir + "/" + name, rel, size, offset)) finally: w.close() + progress(st["done"], "") + if tasks.empty(): + return 0, st["skipped"], 0 + + # 2) varias conexiones subiendo archivos a la vez + def worker(): + ww = _FtpWriter(cfg, cancel, log) + try: + while st["err"] is None: + try: + rpath, rel, size, offset = tasks.get_nowait() + except queue.Empty: + return + if cancel.is_set(): + raise Cancelled() + if offset: + with lock: + st["resumed"] += 1 + log("Continuando %s desde %s MB" % (rel, offset // MiB)) + local = os.path.join(src.path, *rel.split("/")) + ww.begin(rpath, offset) + with lock: + st["done"] += offset # lo que ya habia en la consola cuenta como hecho + pos = offset + with open(local, "rb") as fh: + fh.seek(offset) + while True: + if cancel.is_set() or st["err"] is not None: + raise Cancelled() + chunk = fh.read(2 * MiB) + if not chunk: + break + ww.write(chunk) + pos += len(chunk) + with lock: + st["done"] += len(chunk) + d = st["done"] + progress(d, rel) + ww.end(size) + with lock: + st["sent"] += 1 + except Exception as e: + with lock: + if st["err"] is None: + st["err"] = e + finally: + ww.close() + + ths = [threading.Thread(target=worker, daemon=True) for _ in range(min(workers, tasks.qsize()))] + for t in ths: + t.start() + for t in ths: + t.join() + if st["err"] is not None: + if cancel.is_set() or isinstance(st["err"], Cancelled): + raise Cancelled() + raise st["err"] + return st["sent"], st["skipped"], st["resumed"] def ftp_download(cfg, remote, local, cancel, progress, size=None): @@ -1402,3 +1532,76 @@ def stream_extract_7z(exe, archive, entries, password, remote_root, cfg, cancel, proc.kill() except Exception: pass + + +class _CountReader: + """Envuelve un archivo y cuenta lo leido (para el progreso del .tar.gz).""" + def __init__(self, f): + self.f, self.n = f, 0 + + def read(self, k=-1): + d = self.f.read(k) + self.n += len(d) + return d + + +def stream_extract_tar(path, remote_root, cfg, cancel, log, progress, on_file=None, skip=0): + """Descomprime un .tar / .tar.gz / .tar.bz2 / .tar.xz del PC sobre la marcha y sube cada + archivo a remote_root. progress(bytes_del_comprimido_leidos, total_comprimido, ruta). + skip: miembros ya subidos antes (se leen y se descartan). + Devuelve (archivos, bytes, {ruta de sce_sys/param.json: contenido}).""" + import tarfile + total = os.path.getsize(path) + nfiles = nbytes = 0 + params = {} + w = _FtpWriter(cfg, cancel, log) + try: + with open(path, "rb") as raw: + cr = _CountReader(raw) + with tarfile.open(fileobj=cr, mode="r|*") as tf: + for i, m in enumerate(tf): + if cancel.is_set(): + raise Cancelled() + parts = [x for x in m.name.replace("\\", "/").split("/") if x not in ("", ".")] + if not parts or ".." in parts: + raise PipeError("Ruta peligrosa dentro del comprimido: %s" % m.name) + remote = remote_root + "/" + "/".join(parts) + rel = "/".join(parts) + if m.isfile() and rel.lower().endswith("sce_sys/param.json") and m.size < MiB: + params[rel] = tf.extractfile(m).read() + if i < skip: + continue + w.begin(remote) + w.write(params[rel]) + w.end(m.size) + nfiles += 1 + nbytes += m.size + if on_file: + on_file(i + 1) + continue + if i < skip: + continue + if m.isdir(): + w.mkdir(remote) + elif m.isfile(): + fh = tf.extractfile(m) + w.begin(remote) + while True: + data = fh.read(4 * MiB) + if not data: + break + w.write(data) + progress(cr.n, total, m.name) + w.end(m.size) + nfiles += 1 + nbytes += m.size + else: + log("Me salto %s (enlace o archivo especial)" % m.name) + if on_file: + on_file(i + 1) + progress(cr.n, total, m.name) + except tarfile.TarError as e: + raise PipeError("El .tar esta danado o no es un tar: %s" % e) + finally: + w.close() + return nfiles, nbytes, params diff --git a/icon128.png b/icon128.png new file mode 100644 index 0000000..85c4fb8 Binary files /dev/null and b/icon128.png differ diff --git a/icon16.png b/icon16.png new file mode 100644 index 0000000..b6a8ca2 Binary files /dev/null and b/icon16.png differ diff --git a/icon32.png b/icon32.png new file mode 100644 index 0000000..ef53dac Binary files /dev/null and b/icon32.png differ diff --git a/icon48.png b/icon48.png new file mode 100644 index 0000000..0a3ac25 Binary files /dev/null and b/icon48.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..8aaf489 --- /dev/null +++ b/index.html @@ -0,0 +1,981 @@ + + + + + +PS5 Directo + + + + +
+ +
+

PS5 Directo

+
Envía archivos a tu consola por la red local, sin pasar por el disco del PC
+
+
+ FTP + BFpilot + + + + + + + +
+ + +
+
+

Nueva transferencia

+ + +
Archivos de tu PC: suéltalos en esta página y los busco en tu PC, o arrástralos sobre el icono de PS5 Directo, o clic derecho → Enviar a → PS5 Directo.
+
+ + + Una carpeta se manda tal cual (si tiene sce_sys, como aplicación) y salta lo que ya esté igual en la consola. +
+
+
+ + +
+
+ + + +
+
+ + +
+
+
+ + 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). +
+
+ +
+
+

Cola

+
+ +
+
+
+
+ +
+
+
+

Archivos de la consola

+
+
+
+
+ + + + + + + +
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + +
NombreTamañoModificadoPermisos
+

+
+
+ +
+
+

Actividad

+

Historial de transferencias terminadas o con error (las últimas 500).

+ +
FechaNombreResultadoTamañoDuraciónDestino
+
+
+ + +

Test de velocidad

+
+

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.

+
+
+
+ + +

Ajustes

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Para incluir la descarga, usa "Test de velocidad" en una transferencia de la cola.

+

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.

+
+ + +
+ + +
+ Descomprimir en el PC +

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).

+
+
+
+
+
+
+
+ + + + +
+
+ Extensión para capturar enlaces (Chrome / Edge) +

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.
+ Instalarla (una vez): 1) Abre chrome://extensions (o edge://extensions) · 2) Activa Modo de desarrollador · 3) Cargar descomprimida y elige la carpeta extension de PS5 Directo.

+ +
+
+ Actualizar PS5 Directo +

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.

+ + +
+ +
+
+
+ Permisos de las aplicaciones +

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.

+ +
+
+
+ Limpieza de la PS5 +

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.

+ +
+
+
+

Necesitas en la PS5: el servidor FTP (puerto 2121) y BFpilot cargado desde Payload Manager (puerto 5905). Para ir más rápido, con Wi-Fi baja “Trozo” a 2–4 MB.

+
+
+ +
+ + + + + + + + + + + diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..7bc157e --- /dev/null +++ b/logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ed55e58 --- /dev/null +++ b/manifest.json @@ -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" } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..d6b9114 --- /dev/null +++ b/popup.html @@ -0,0 +1,20 @@ + + + + +

PS5 Directo

+
+
Comprobando…
+
+
+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. + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..273579c --- /dev/null +++ b/popup.js @@ -0,0 +1,42 @@ +const EN = !(navigator.language || "es").toLowerCase().startsWith("es"); +const T = EN ? { + capture: "Capture links", checking: "Checking…", reset: "Reset counter", + help: "Pass the captcha and click Download as usual: if it's a .rar, .7z, .zip, .pkg… and PS5 Directo is open, the link goes to its link box and the browser downloads nothing. If PS5 Directo is closed, the download goes ahead normally.", + sent: "Sent", last: "last", connected: "● Connected", to: "to PS5 Directo", closed: "● PS5 Directo is not open", normal: "(downloads will work normally)" +} : { + capture: "Capturar enlaces", checking: "Comprobando…", reset: "Poner contador a 0", + help: null, sent: "Enviados", last: "último", connected: "● Conectado", to: "a PS5 Directo", closed: "● PS5 Directo no está abierto", normal: "(las descargas seguirán normales)" +}; +if (EN) { + document.documentElement.lang = "en"; + document.getElementById("lblEn").textContent = T.capture; + document.getElementById("st").textContent = T.checking; + document.getElementById("reset").textContent = T.reset; + document.getElementById("help").textContent = T.help; +} +async function findApp() { + for (const port of [8777, 8778, 8779]) { + try { + const r = await fetch(`http://127.0.0.1:${port}/api/ext/ping`, { cache: "no-store" }); + if (r.ok) { const j = await r.json(); if (j.app === "PS5 Directo") return j; } + } catch (e) {} + } + return null; +} +(async () => { + const cfg = await chrome.storage.local.get({ enabled: true, captured: 0, last: "" }); + const en = document.getElementById("en"); + en.checked = cfg.enabled; + en.onchange = () => chrome.storage.local.set({ enabled: en.checked }); + document.getElementById("cnt").textContent = + cfg.captured ? `${T.sent}: ${cfg.captured}${cfg.last ? " (" + T.last + ": " + cfg.last + ")" : ""}` : ""; + document.getElementById("reset").onclick = async () => { + await chrome.storage.local.set({ captured: 0, last: "" }); + chrome.action.setBadgeText({ text: "" }); + document.getElementById("cnt").textContent = ""; + }; + const app = await findApp(); + const st = document.getElementById("st"); + if (app) { st.innerHTML = `${T.connected} ${T.to} v${app.version}`; chrome.action.setBadgeText({ text: "" }); } + else st.innerHTML = `${T.closed} ${T.normal}`; +})(); diff --git a/ps5directo.ico b/ps5directo.ico new file mode 100644 index 0000000..04a48ee Binary files /dev/null and b/ps5directo.ico differ