diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..0086358
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
diff --git a/.github/ISSUE_TEMPLATE/mac.md b/.github/ISSUE_TEMPLATE/mac.md
new file mode 100644
index 0000000..b5560ca
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/mac.md
@@ -0,0 +1,18 @@
+---
+name: Mac (versión en pruebas / testing)
+about: Cuéntanos cómo va PS5 Directo en tu Mac / Tell us how PS5 Directo works on your Mac
+title: "[Mac] "
+labels: mac
+---
+
+**Mac y macOS / Mac and macOS version:** (por ejemplo / e.g. MacBook Air M2, macOS 15.1)
+
+**¿Abre? / Does it open?** (sí / no / con avisos — yes / no / with warnings)
+
+**¿Qué funciona y qué falla? / What works and what fails?**
+
+
+**Informe de fallo / Problem report:** en el programa pulsa «Crear informe de fallo» y arrastra aquí el .zip / in the program click "Create problem report" and drag the .zip here.
+
+Si no llega a abrirse, adjunta si puedes / If it doesn't open, please attach if you can:
+`~/Library/Application Support/PS5 Directo/lanzador.log` y / and `~/.ps5directo/ps5directo.log`
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 30e69c2..a647d51 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,6 +3,8 @@
# - PS5Directo-X.Y.Z-Setup.exe el instalador: doble clic, «Instalar» y listo. Es la descarga normal.
# - PS5Directo-X.Y.Z-Windows.zip lo mismo sin instalar (con Python incluido). Tambien lo usa
# «Actualizar» dentro del programa, que solo copia los archivos.
+# - PS5Directo-X.Y.Z-Linux.tar.gz Linux x86-64, con Python y 7-Zip incluidos (se abre con ps5directo.sh).
+# - PS5Directo-X.Y.Z-macOS.dmg Mac (Apple Silicon e Intel), en pruebas: la app con Python y 7-Zip.
name: Publicar version
on:
@@ -47,6 +49,35 @@ jobs:
- name: Zip para Windows
run: (cd dist && zip -qr "PS5Directo-${{ steps.v.outputs.ver }}-Windows.zip" PS5Directo)
+ # Linux (x86-64): la app con Python y 7-Zip incluidos. Se abre con ps5directo.sh y la primera
+ # vez se añade sola al menu de aplicaciones.
+ - name: Paquete para Linux
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -e
+ L=build/linux/PS5Directo
+ mkdir -p "$L"
+ git archive HEAD | tar -x -C "$L"
+ rm -rf "$L/.github" "$L/installer" "$L"/*.bat "$L"/*.vbs
+ chmod +x "$L/ps5directo.sh"
+ PBS=20250604; PYV=3.12.11
+ curl -fsSL -o pyl.tgz "https://github.com/astral-sh/python-build-standalone/releases/download/$PBS/cpython-$PYV+$PBS-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
+ tar -xzf pyl.tgz -C "$L"
+ P="$L/python/lib/python3.12"
+ rm -rf "$P/test" "$P/idlelib" "$P/ensurepip" "$P/lib2to3" "$P/pydoc_data" "$P/turtledemo" \
+ "$P"/site-packages/pip* "$P/tkinter/test" "$L/python/share" "$L/python/include"
+ find "$L/python" -name __pycache__ -prune -exec rm -rf {} +
+ "$L/python/bin/python3" -c "import ssl, sqlite3, ftplib, tkinter, lzma, bz2, ctypes; print('python ok')"
+ url=$(gh api repos/ip7z/7zip/releases/latest --jq '.assets[] | select(.name | test("linux-x64\\.tar\\.xz$")) | .browser_download_url' | head -1)
+ mkdir -p "$L/bin" sz
+ curl -fsSL -o sz.tar.xz "$url"
+ tar -xJf sz.tar.xz -C sz
+ cp sz/7zz "$L/bin/7zz"
+ chmod +x "$L/bin/7zz"
+ (cd build/linux && tar -czf "../../dist/PS5Directo-${{ steps.v.outputs.ver }}-Linux.tar.gz" PS5Directo)
+ ls -la dist
+
- uses: actions/upload-artifact@v4
with:
name: dist
@@ -73,6 +104,102 @@ jobs:
if ($LASTEXITCODE -ne 0) { exit 1 }
Get-ChildItem dist\*.exe, dist\*.zip | Format-Table Name, Length
+ - uses: actions/upload-artifact@v4
+ with:
+ name: setup
+ path: dist/PS5Directo-*-Setup.exe
+ retention-days: 3
+
+ # Mac: "PS5 Directo.app" con Python para Apple Silicon e Intel y 7-Zip, dentro de un .dmg.
+ # El lanzador (installer/macos/launcher.sh) copia el programa a ~/Library/Application Support
+ # para que «Actualizar» funcione. Firmada solo "ad hoc" (sin cuenta de desarrollador de Apple).
+ mac:
+ needs: build
+ runs-on: macos-latest
+ env:
+ VER: ${{ needs.build.outputs.ver }}
+ GH_TOKEN: ${{ github.token }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Montar la app
+ run: |
+ set -e
+ A="build/PS5 Directo.app"; C="$A/Contents"; R="$C/Resources"
+ mkdir -p "$C/MacOS" "$R/app" "$R/bin"
+ git archive HEAD | tar -x -C "$R/app"
+ rm -rf "$R/app/.github" "$R/app/installer" "$R/app"/*.bat "$R/app"/*.vbs "$R/app/ps5directo.sh"
+ cp installer/macos/launcher.sh "$C/MacOS/PS5 Directo"
+ chmod +x "$C/MacOS/PS5 Directo"
+ sed "s/__VERSION__/$VER/g" installer/macos/Info.plist > "$C/Info.plist"
+ plutil -lint "$C/Info.plist"
+ # icono
+ mkdir -p ps5.iconset
+ for s in 16 32 128 256 512; do
+ sips -z $s $s web/icon-512.png --out "ps5.iconset/icon_${s}x${s}.png" >/dev/null
+ d=$((s*2)); sips -z $d $d web/icon-512.png --out "ps5.iconset/icon_${s}x${s}@2x.png" >/dev/null
+ done
+ iconutil -c icns ps5.iconset -o "$R/ps5directo.icns"
+ # Python para cada procesador
+ PBS=20250604; PYV=3.12.11
+ for pair in aarch64:arm64 x86_64:x86_64; do
+ src=${pair%%:*}; dst=${pair##*:}
+ curl -fsSL -o py.tgz "https://github.com/astral-sh/python-build-standalone/releases/download/$PBS/cpython-$PYV+$PBS-$src-apple-darwin-install_only_stripped.tar.gz"
+ rm -rf python && tar -xzf py.tgz
+ P=python/lib/python3.12
+ rm -rf "$P/test" "$P/idlelib" "$P/ensurepip" "$P/lib2to3" "$P/pydoc_data" "$P/turtledemo" \
+ "$P"/site-packages/pip* "$P/tkinter/test" python/share python/include
+ find python -name __pycache__ -prune -exec rm -rf {} +
+ mv python "$R/python-$dst"
+ done
+ "$R/python-$(uname -m)/bin/python3" -c "import ssl, sqlite3, ftplib, lzma, bz2, ctypes; print('python ok')"
+ # 7-Zip
+ url=$(gh api repos/ip7z/7zip/releases/latest --jq '.assets[] | select(.name | test("mac\\.tar\\.xz$")) | .browser_download_url' | head -1)
+ mkdir -p sz && curl -fsSL -o sz.tar.xz "$url" && tar -xJf sz.tar.xz -C sz
+ cp sz/7zz "$R/bin/7zz" && chmod +x "$R/bin/7zz"
+ "$R/bin/7zz" | head -2
+ codesign --force --deep --sign - "$A"
+ codesign --verify --deep --verbose=1 "$A"
+
+ - name: Prueba de arranque en un Mac
+ run: |
+ export HOME="$RUNNER_TEMP/casa"; mkdir -p "$HOME"
+ "build/PS5 Directo.app/Contents/MacOS/PS5 Directo" --no-browser &
+ ok=""
+ for i in $(seq 1 30); do
+ sleep 1
+ if grep -q "PS5 Directo $VER" "$HOME/.ps5directo/ps5directo.log" 2>/dev/null; then ok=1; break; fi
+ done
+ cat "$HOME/.ps5directo/ps5directo.log" || true
+ cat "$HOME/Library/Application Support/PS5 Directo/lanzador.log" || true
+ pkill -f "PS5 Directo/app/app.py" || true
+ test -n "$ok"
+
+ - name: Crear el .dmg
+ run: |
+ set -e
+ mkdir -p dmg dist
+ cp -R "build/PS5 Directo.app" dmg/
+ ln -s /Applications dmg/Applications
+ cp installer/macos/LEEME-Mac.txt "dmg/Si no abre - LEEME.txt"
+ hdiutil create -volname "PS5 Directo" -srcfolder dmg -ov -format UDZO "dist/PS5Directo-$VER-macOS.dmg"
+ ls -la dist
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: mac
+ path: dist/PS5Directo-*-macOS.dmg
+ retention-days: 3
+
+ release:
+ needs: [build, setup, mac]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ path: dist
+ merge-multiple: true
+
# Con una etiqueta crea la release. Pulsando "Run workflow" en Actions añade (o rehace) los archivos
# de la release de la version que pone app.py, aunque ya exista.
- uses: softprops/action-gh-release@v2
@@ -81,3 +208,5 @@ jobs:
files: |
dist/PS5Directo-*-Setup.exe
dist/PS5Directo-*-Windows.zip
+ dist/PS5Directo-*-Linux.tar.gz
+ dist/PS5Directo-*-macOS.dmg
diff --git a/LEEME.txt b/LEEME.txt
index 934c43e..a35bad0 100644
--- a/LEEME.txt
+++ b/LEEME.txt
@@ -305,8 +305,38 @@ QUÉ DESCARGAR
prefiera descomprimirlo en una carpeta. También lo usa "Actualizar".
Para quitarlo: Configuración de Windows > Aplicaciones > PS5 Directo >
Desinstalar. Tus ajustes se conservan por si vuelves a instalarlo.
+- PS5Directo-X.Y.Z-macOS.dmg: para Mac (Apple Silicon e Intel, macOS 11
+ o posterior). EN PRUEBAS: todavía no se ha probado en un Mac de verdad.
+ Ábrelo y arrastra PS5 Directo a Aplicaciones. La primera vez macOS la
+ bloquea porque no está firmada por Apple: en macOS 15 o posterior,
+ Ajustes del Sistema > Privacidad y seguridad > "Abrir igualmente"; en
+ anteriores, clic derecho en la app > Abrir. Si algo falla, "Crear
+ informe de fallo" y adjúntalo en github.com/jstrdev/ps5directo/issues.
+- PS5Directo-X.Y.Z-Linux.tar.gz: para Linux (x86-64, distribuciones
+ recientes como Ubuntu 22.04 o posterior). Descomprímelo donde quieras
+ y ábrelo una vez con ./ps5directo.sh en una terminal (o clic derecho
+ > Ejecutar como programa). Esa vez se añade al menú de aplicaciones y
+ desde entonces lo abres desde ahí. Trae Python y 7-Zip.
+ Para la ventana sin pestañas usa Chrome, Chromium, Edge o Brave si los
+ tienes (si no, se abre en tu navegador). Para los torrents hace falta
+ aria2: sudo apt install aria2. En Linux no hay "Enviar a" ni el aviso
+ del cortafuegos de Windows.
+NOVEDADES 1.21.0
+- Versión para Mac EN PRUEBAS (PS5Directo-1.21.0-macOS.dmg), para Apple
+ Silicon e Intel, con Python y 7-Zip incluidos. Aún no se ha probado en
+ un Mac de verdad: se agradece cualquier aviso en GitHub.
+- El informe de fallo incluye ahora el registro técnico del programa
+ (sin el código de acceso de la sesión).
+- Arreglado: si cerrabas el programa en la Biblioteca, al abrirlo salía
+ marcada «Transferencias» con la Biblioteca vacía debajo.
+- Versión para Linux (PS5Directo-1.21.0-Linux.tar.gz), con Python y 7-Zip
+ incluidos. Se añade sola al menú de aplicaciones y se actualiza igual
+ que en Windows.
+- Las descargas al PC van a tu carpeta de Descargas real (en Linux puede
+ llamarse "Descargas").
+
NOVEDADES 1.20.4
- Icono propio en la barra de tareas: la ventana ya no se mete dentro del
icono de Edge o Chrome, y al anclarla se ancla PS5 Directo.
@@ -314,6 +344,14 @@ NOVEDADES 1.20.4
tenías en vez de abrir otra.
- Si lo anclaste antes de esta versión, desáncla el icono viejo y ancla
el nuevo (clic derecho en el icono de la barra > Anclar).
+- Capturas: salen también las hechas con la pantalla en HDR (.jxr), que se
+ ven convertidas a JPG (al bajarlas al PC se guarda el original). Cada una
+ dice de qué aplicación es y la fecha, los vídeos llevan la portada de la
+ aplicación y ya no salen las copias temporales que hace la consola al
+ compartir.
+- Arreglado: en «Archivos de la consola», calcular el tamaño de una
+ carpeta, borrarla, copiarla, arreglar sus permisos y «Reemplazar» una
+ carpeta que ya existía fallaban desde la 1.20.3.
NOVEDADES 1.20.3
- Instalador para Windows (PS5Directo-1.20.3-Setup.exe): doble clic,
diff --git a/README.md b/README.md
index cea254c..b984714 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,8 @@ An all-in-one Windows tool for jailbroken PS5 consoles: it **downloads, converts
## Requirements
- **PC:** Windows 10/11. Nothing else to install: the download includes everything it needs. Optional: [7-Zip](https://www.7-zip.org/), only if you want to extract on the PC instead of on the console.
+- **Mac (new, in testing — not tested on a real Mac yet, [feedback welcome](../../issues/new?template=mac.md)):** macOS 11 or later, Apple Silicon or Intel. Python and 7-Zip are included. Torrents need `aria2` (`brew install aria2`).
+- **Linux (new, x86-64):** any recent distro (glibc 2.35+, e.g. Ubuntu 22.04+). Python and 7-Zip are included. For the app window without tabs, Chrome, Chromium, Edge or Brave (otherwise it opens in your default browser). Torrents need `aria2` (`sudo apt install aria2`).
- **Jailbroken PS5:**
- An FTP server, for example [ftpsrv](https://github.com/ps5-payload-dev/ftpsrv) on port 2121.
- [BFpilot](https://github.com/ItsBlurf/BFpilot) on port 5905.
@@ -48,13 +50,17 @@ An all-in-one Windows tool for jailbroken PS5 consoles: it **downloads, converts
>
> Rather not install? Download `PS5Directo-X.Y.Z-Windows.zip` instead, extract it anywhere and run `Crear accesos directos.bat`. Both update themselves the same way.
+**Mac (in testing):** download `PS5Directo-X.Y.Z-macOS.dmg`, open it and drag **PS5 Directo** into Applications. The app isn't signed by Apple, so the first time macOS blocks it: on macOS 15+ go to *System Settings → Privacy & Security → Open Anyway*; on older versions right-click the app → *Open*. **It hasn't been tested on a real Mac yet:** if something fails, click *Create problem report* in the program and attach it to an [issue](../../issues/new?template=mac.md). Thanks!
+
+**Linux:** download `PS5Directo-X.Y.Z-Linux.tar.gz`, extract it anywhere (for example in your home folder) and open it once with `./ps5directo.sh` in a terminal (or right-click → *Run as a Program*). That first time it adds **PS5 Directo** to your applications menu, so from then on you open it from there. It updates itself like on Windows.
+
> **About BFpilot and permissions:** up to at least v0.4.4, BFpilot's archive extractor leaves files without the execute bit, so the PS5 refuses to launch them (CE-107750-0). This also happens when you extract from BFpilot's own web UI. PS5 Directo fixes it automatically after every extraction. For content you already have, use **Settings → Fix permissions**.
The full guide is in [README_EN.txt](README_EN.txt) (Spanish: [LEEME.txt](LEEME.txt)).
## Publishing a release
-Bump `VERSION` in `app.py`, add the notes to `LEEME.txt` / `README_EN.txt`, then push a tag (`git tag v1.20.3 && git push --tags`). The GitHub Action builds the installer `PS5Directo-X.Y.Z-Setup.exe` and `PS5Directo-X.Y.Z-Windows.zip` (both with Python included) and creates the release. The installer script is `installer/PS5Directo.iss` (Inno Setup).
+Bump `VERSION` in `app.py`, add the notes to `LEEME.txt` / `README_EN.txt`, then push a tag (`git tag v1.20.3 && git push --tags`). The GitHub Action builds the installer `PS5Directo-X.Y.Z-Setup.exe`, `PS5Directo-X.Y.Z-Windows.zip`, `PS5Directo-X.Y.Z-Linux.tar.gz` and `PS5Directo-X.Y.Z-macOS.dmg` (all with Python included; the Mac app is built and smoke-tested on a GitHub macOS runner) and creates the release. The installer script is `installer/PS5Directo.iss` (Inno Setup).
## Disclaimer
@@ -88,4 +94,8 @@ Herramienta todo en uno para Windows y PS5 con jailbreak: **descarga, convierte
Si Windows dice «Windows protegió su PC», pulsa **Más información → Ejecutar de todos modos** (el instalador no está firmado, que cuesta dinero; es el aviso normal en programas pequeños y gratuitos). ¿Prefieres no instalar? Descarga el `.zip`, descomprímelo y ejecuta `Crear accesos directos.bat`. La guía completa está en [LEEME.txt](LEEME.txt).
+**Mac (nuevo, en pruebas):** descarga `PS5Directo-X.Y.Z-macOS.dmg`, ábrelo y arrastra **PS5 Directo** a Aplicaciones. No está firmada por Apple, así que la primera vez macOS la bloquea: en macOS 15 o posterior ve a *Ajustes del Sistema → Privacidad y seguridad → Abrir igualmente*; en versiones anteriores, clic derecho en la app → *Abrir*. **Todavía no se ha podido probar en un Mac de verdad:** si algo falla, pulsa *Crear informe de fallo* en el programa y adjúntalo en un [aviso](../../issues/new?template=mac.md). ¡Gracias!
+
+**Linux (nuevo):** descarga `PS5Directo-X.Y.Z-Linux.tar.gz`, descomprímelo donde quieras y ábrelo una vez con `./ps5directo.sh` en una terminal (o clic derecho → *Ejecutar como programa*). Esa primera vez se añade solo al menú de aplicaciones, y desde entonces lo abres desde ahí. Trae Python y 7-Zip; para los torrents hace falta `aria2` (`sudo apt install aria2`).
+
**Aviso:** proyecto independiente, sin relación con Sony ni con los autores de las herramientas citadas. No incluye contenido de terceros, claves ni archivos de Sony, ni enlaces a sitios de descargas. Úsalo solo con contenido que tengas derecho a usar (copias de seguridad propias y homebrew). No hace el jailbreak ni salta ninguna protección: solo copia archivos a una consola que ya has modificado tú. No se da ayuda para conseguir contenido sin licencia. Se ofrece tal cual, sin garantías, y lo que hagas con él es responsabilidad tuya.
diff --git a/README_EN.txt b/README_EN.txt
index f863721..b792b10 100644
--- a/README_EN.txt
+++ b/README_EN.txt
@@ -319,15 +319,50 @@ WHAT TO DOWNLOAD
rather extract it to a folder. "Update" uses it too.
To remove it: Windows Settings > Apps > PS5 Directo > Uninstall. Your
settings are kept in case you install it again.
+- PS5Directo-X.Y.Z-macOS.dmg: for Mac (Apple Silicon and Intel, macOS 11
+ or later). IN TESTING: not tested on a real Mac yet. Open it and drag
+ PS5 Directo into Applications. The first time macOS blocks it because
+ it isn't signed by Apple: on macOS 15 or later, System Settings >
+ Privacy & Security > "Open Anyway"; on older ones, right-click the app
+ > Open. If something fails, "Create problem report" and attach it at
+ github.com/jstrdev/ps5directo/issues.
+- PS5Directo-X.Y.Z-Linux.tar.gz: for Linux (x86-64, recent distros such
+ as Ubuntu 22.04 or later). Extract it anywhere and open it once with
+ ./ps5directo.sh in a terminal (or right-click > Run as a Program).
+ That first time it adds itself to the applications menu, so from then
+ on you open it from there. Python and 7-Zip are included.
+ For the window without tabs it uses Chrome, Chromium, Edge or Brave if
+ you have one (otherwise your browser). Torrents need aria2:
+ sudo apt install aria2. Linux has no "Send to" and no Windows firewall
+ prompt.
WHAT'S NEW
----------
+1.21.0 - Mac version IN TESTING (PS5Directo-1.21.0-macOS.dmg), for Apple
+ Silicon and Intel, with Python and 7-Zip included. Not tested on
+ a real Mac yet: any report on GitHub is welcome.
+ - The problem report now includes the program's technical log
+ (without the session access code).
+ - Fixed: if you closed the program on the Library tab, on reopening
+ it showed "Transfers" selected with an empty Library below.
+ - Linux version (PS5Directo-1.21.0-Linux.tar.gz), with Python and
+ 7-Zip included. It adds itself to the applications menu and
+ updates itself like on Windows.
+ - Downloads to the PC go to your real Downloads folder (on Linux it
+ may have another name).
+
1.20.4 - Its own taskbar icon: the window no longer hides inside the Edge
or Chrome icon, and pinning it pins PS5 Directo.
- If it's already open and you open it again, it brings the existing
window to the front instead of opening another one.
- If you pinned it before this version, unpin the old icon and pin
the new one (right-click the taskbar icon > Pin).
+ - Captures: screenshots taken with HDR on (.jxr) now show up,
+ previewed as JPG (downloading keeps the original). Each one shows
+ the app and the date, videos show the app's cover, and the
+ temporary copies the console makes when sharing are hidden.
+ - Fixed: in "Console files", folder size, deleting, copying, fixing
+ permissions and "Replace" on an existing folder failed since 1.20.3.
1.20.3 - Windows installer (PS5Directo-1.20.3-Setup.exe): double-click,
"Install", done, everything included. The .zip is still there if
diff --git a/app.py b/app.py
index a628650..6f003c6 100644
--- a/app.py
+++ b/app.py
@@ -24,7 +24,7 @@ sys.path.insert(0, HERE)
import engine as E # noqa: E402
import features as F # noqa: E402
-VERSION = "1.20.4"
+VERSION = "1.21.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"
@@ -1238,7 +1238,7 @@ def fm_extract(src, folder, password="", exists="keepboth"):
def fm_download(paths):
cfg = cfg_snapshot()
paths = [fm_clean(x) for x in paths]
- root_local = os.path.join(os.path.expanduser("~"), "Downloads", "PS5 Directo")
+ root_local = os.path.join(downloads_dir(), "PS5 Directo")
def walk(rpath):
"""[(ruta remota, ruta relativa, tamano)] de un archivo o carpeta."""
@@ -1535,8 +1535,7 @@ def fm_api(p, b):
return {"path": fp, "name": os.path.basename(fp)}
if p == "/api/report/open":
os.makedirs(report_dir(), exist_ok=True)
- if os.name == "nt":
- os.startfile(report_dir())
+ _open_quiet(report_dir())
return {"dir": report_dir()}
if p == "/api/lib/check":
return launch_check(str(b.get("path") or ""), int(b.get("minutes") or 30))
@@ -1597,14 +1596,16 @@ def fm_api(p, b):
if p == "/api/bk/restore":
if BK["busy"]:
raise E.PipeError("Espera a que termine la copia en marcha")
- threading.Thread(target=backup_restore, args=(str(b.get("path") or ""), b.get("ids") or []),
- daemon=True).start()
+ bpath = os.path.abspath(str(b.get("path") or ""))
+ if not bpath.startswith(os.path.abspath(backup_dir(cfg_snapshot()))) or not os.path.isfile(bpath):
+ raise E.PipeError("Esa copia ya no está en la carpeta de copias (¿la moviste o borraste?). "
+ "Pulsa Actualizar en la lista de copias.")
+ threading.Thread(target=backup_restore, args=(bpath, b.get("ids") or []), daemon=True).start()
return {}
if p == "/api/bk/open":
d = backup_dir()
os.makedirs(d, exist_ok=True)
- if os.name == "nt":
- os.startfile(d)
+ _open_quiet(d)
return {"dir": d}
if p == "/api/gu/status":
return gu_status()
@@ -1660,9 +1661,9 @@ def fm_api(p, b):
FM_CANCEL.set()
return {}
if p == "/api/fm/open_local":
- path = FMOP.get("local") or os.path.join(os.path.expanduser("~"), "Downloads", "PS5 Directo")
- if os.name == "nt" and os.path.isdir(path):
- os.startfile(path)
+ path = FMOP.get("local") or os.path.join(downloads_dir(), "PS5 Directo")
+ if os.path.isdir(path):
+ _open_quiet(path)
return {"path": path}
if p == "/api/pick":
return {"paths": pick_paths(b.get("kind") or "files", b.get("title") or "")}
@@ -1707,14 +1708,49 @@ def fm_api(p, b):
raise E.PipeError("no existe")
+_DL_DIR = []
+
+
+def downloads_dir():
+ """Carpeta de Descargas del usuario. En Linux puede llamarse "Descargas" (xdg-user-dir)."""
+ if _DL_DIR:
+ return _DL_DIR[0]
+ d = os.path.join(os.path.expanduser("~"), "Downloads")
+ if sys.platform.startswith("linux"):
+ try:
+ import subprocess
+ r = subprocess.run(["xdg-user-dir", "DOWNLOAD"], capture_output=True, timeout=5)
+ x = r.stdout.decode("utf-8", "replace").strip()
+ if r.returncode == 0 and x and x != os.path.expanduser("~") and os.path.isdir(x):
+ d = x
+ except Exception:
+ pass
+ _DL_DIR.append(d)
+ return d
+
+
+def _open_quiet(path):
+ try:
+ F.open_path(path)
+ except Exception as e:
+ print("No pude abrir %s: %s" % (path, e))
+
+
def pick_paths(kind, title):
- """Abre el dialogo de Windows para elegir archivos o una carpeta del PC."""
+ """Abre el dialogo del sistema para elegir archivos o una carpeta del PC."""
import subprocess
if os.name == "nt":
try:
return F.pick_windows(kind, title)
except Exception as e:
print("Selector de PowerShell:", e)
+ else:
+ try:
+ r = F.pick_native(kind, title)
+ if r is not None:
+ return r
+ except Exception as e:
+ print("Selector del sistema:", e)
exe = sys.executable
if os.name == "nt" and exe.lower().endswith("pythonw.exe"):
alt = exe[:-len("pythonw.exe")] + "python.exe"
@@ -1848,6 +1884,7 @@ COVER_DIR = os.path.join(DATA_DIR, "covers")
MEDIA_DIR = os.path.join(DATA_DIR, "media")
MEDIA_ROOT = "/user/av_contents"
MEDIA_TYPES = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp",
+ ".jxr": "image/vnd.ms-photo", # capturas con la pantalla en HDR
".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/quicktime"}
@@ -1952,7 +1989,7 @@ def _job_name(j):
def report_dir():
- return os.path.join(os.path.expanduser("~"), "Downloads", "PS5 Directo", "Informes")
+ return os.path.join(downloads_dir(), "PS5 Directo", "Informes")
def make_report(job_id=None):
@@ -1997,6 +2034,13 @@ def make_report(job_id=None):
z.writestr("actividad.json", json.dumps(read_history()[-60:], ensure_ascii=False, indent=1))
with NOTIF_LOCK:
z.writestr("avisos.json", json.dumps(NOTIFS[:60], ensure_ascii=False, indent=1))
+ try: # registro tecnico del programa (sin el codigo de acceso de la sesion)
+ with open(os.path.join(DATA_DIR, "ps5directo.log"), encoding="utf-8", errors="replace") as fh:
+ lines = fh.readlines()[-400:]
+ z.writestr("registro_del_programa.txt",
+ "".join(re.sub(r"([#?&]t=)[\w-]+", r"\1(oculto)", ln) for ln in lines))
+ except OSError:
+ pass
z.writestr("LEEME.txt", "Informe de PS5 Directo %s (%s)\n\n%s\n\nNo lleva contraseñas. Pásaselo a quien te "
"ayude.\n" % (VERSION, info["date"], "\n".join(summary)))
return fp
@@ -2281,23 +2325,64 @@ def col_icon(path):
def media_list():
- """Capturas y videos de la consola (los mas nuevos primero)."""
+ """Capturas y videos de la consola (los mas nuevos primero). La PS5 los guarda en
+ /user/av_contents/{photo,video}/NPXS40087/
//.jpg|jxr|webm."""
cfg = cfg_snapshot()
out = []
- files = E.Ftp(cfg).call(lambda f: _ftp_walk(f, MEDIA_ROOT, [], threading.Event()))
+ files = E.Ftp(cfg).call(lambda f: _ftp_walk_sizes(f, MEDIA_ROOT, [], threading.Event()))
+ titles = lib_load().get("titles") or {}
for p, sz in files:
ext = posixpath.splitext(p)[1].lower()
- if ext not in MEDIA_TYPES or "/thumbnail" in p.lower():
+ low = p.lower()
+ # miniaturas y copias temporales que hace la consola al compartir
+ if ext not in MEDIA_TYPES or "/thumbnail" in low or "/content_tmp/" in low:
continue
- out.append({"path": p, "name": posixpath.basename(p), "size": sz,
- "video": MEDIA_TYPES[ext].startswith("video"),
- "folder": posixpath.basename(posixpath.dirname(p))})
- out.sort(key=lambda x: x["name"], reverse=True)
+ name = posixpath.basename(p)
+ tids = [c for c in p.split("/") if re.match(r"^[A-Z]{4}\d{5}$", c) and c != "NPXS40087"]
+ tid = tids[-1] if tids else ""
+ m = re.search(r"(20\d{2})(\d{2})(\d{2})_?(\d{2})(\d{2})(\d{2})?", name)
+ when = "%s-%s-%sT%s:%s:%s" % (m.group(1), m.group(2), m.group(3), m.group(4), m.group(5),
+ m.group(6) or "00") if m else ""
+ out.append({"path": p, "name": name, "size": sz,
+ "video": MEDIA_TYPES[ext].startswith("video"), "hdr": ext == ".jxr",
+ "tid": tid, "app": (titles.get(tid) or {}).get("title") or "",
+ "when": when, "folder": posixpath.basename(posixpath.dirname(p))})
+ out.sort(key=lambda x: (x["when"] or "", x["name"]), reverse=True)
return out[:3000]
-def media_cached(path):
- """Copia local (cache) de una captura o video, para verla en el navegador."""
+def _jxr_to_jpg(src, dst):
+ """Vista previa de una captura HDR (.jxr): Windows trae el decodificador de JPEG XR
+ (y en Mac se intenta con sips, la herramienta de imagenes del sistema)."""
+ if sys.platform == "darwin":
+ import subprocess
+ try:
+ r = subprocess.run(["sips", "-s", "format", "jpeg", src, "--out", dst], capture_output=True, timeout=60)
+ return r.returncode == 0 and os.path.isfile(dst) and os.path.getsize(dst) > 0
+ except Exception:
+ return False
+ if os.name != "nt":
+ return False
+ q = lambda t: "'" + t.replace("'", "''") + "'"
+ script = (
+ "Add-Type -AssemblyName PresentationCore;"
+ "$s=[IO.File]::OpenRead(%s);"
+ "try{ $d=[System.Windows.Media.Imaging.BitmapDecoder]::Create($s,"
+ " [System.Windows.Media.Imaging.BitmapCreateOptions]::None,"
+ " [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad) } finally { $s.Close() };"
+ "$c=New-Object System.Windows.Media.Imaging.FormatConvertedBitmap($d.Frames[0],"
+ " [System.Windows.Media.PixelFormats]::Bgr24, $null, 0);"
+ "$e=New-Object System.Windows.Media.Imaging.JpegBitmapEncoder; $e.QualityLevel=90;"
+ "$e.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($c));"
+ "$o=[IO.File]::Create(%s); try{ $e.Save($o) } finally { $o.Close() }"
+ ) % (q(src), q(dst))
+ r = _powershell(script, timeout=60)
+ return r.returncode == 0 and os.path.isfile(dst) and os.path.getsize(dst) > 0
+
+
+def media_cached(path, preview=False):
+ """Copia local (cache) de una captura o video, para verla en el navegador. Con preview,
+ una captura HDR (.jxr, que los navegadores no abren) se da convertida a JPG."""
import hashlib
path = fm_clean(path)
if not path.startswith(MEDIA_ROOT + "/"):
@@ -2311,6 +2396,10 @@ def media_cached(path):
with open(fp + ".part", "wb") as fo:
E.Ftp(cfg_snapshot()).call(lambda f: f.retrbinary("RETR " + path, fo.write, 1024 * 1024))
os.replace(fp + ".part", fp)
+ if preview and ext == ".jxr":
+ jpg = fp[:-4] + "_vista.jpg"
+ if os.path.isfile(jpg) or _jxr_to_jpg(fp, jpg):
+ return jpg, "image/jpeg"
return fp, MEDIA_TYPES[ext]
FMT_LABEL = {"plain": "carpeta normal", "ffpkg": "imagen .ffpkg", "exfat": "imagen .exfat", "ffpfs": "imagen .ffpfs",
"ffpfsc": "imagen comprimida .ffpfsc", "fpkg": "paquete .fpkg", "lz4": "carpeta LZ4",
@@ -2478,6 +2567,8 @@ def ensure_aria2(job, cfg):
if w:
return w
if os.name != "nt":
+ if sys.platform == "darwin":
+ raise E.PipeError("Para bajar torrents hace falta aria2. Instálalo con Homebrew (brew.sh): brew install aria2")
raise E.PipeError("Para bajar torrents hace falta aria2 (instálalo, por ejemplo: sudo apt install aria2)")
with ARIA2_LOCK:
if os.path.isfile(mine):
@@ -2975,7 +3066,7 @@ def forge_job(job, srcs, cfg, cancel, src_path, created, pc_folder, up):
def pc_temp_root(cfg):
d = (cfg.get("pc_temp_dir") or "").strip().strip('"')
- return d or os.path.join(os.path.expanduser("~"), "Downloads", "PS5 Directo (temporal)")
+ return d or os.path.join(downloads_dir(), "PS5 Directo (temporal)")
def _pcx_state(job):
@@ -4665,7 +4756,7 @@ def backup_dir(cfg=None):
return d or os.path.join(os.path.expanduser("~"), "Documents", "PS5 Directo", "Copias")
-def _ftp_walk(f, root, out, cancel, limit=None):
+def _ftp_walk_sizes(f, root, out, cancel, limit=None):
"""[(ruta, tamano)] de todos los archivos bajo root (vacio si no existe)."""
try:
listing = E.ftp_listdir(f, root)
@@ -4676,7 +4767,7 @@ def _ftp_walk(f, root, out, cancel, limit=None):
raise E.Cancelled()
p = root.rstrip("/") + "/" + n
if v["type"] == "dir":
- _ftp_walk(f, p, out, cancel, limit)
+ _ftp_walk_sizes(f, p, out, cancel, limit)
elif v["type"] == "file" and (limit is None or (v.get("size") or 0) <= limit):
out.append((p, v.get("size") or 0))
return out
@@ -4720,10 +4811,10 @@ def backup_run(sections=None, label="copia"):
todo = [] # (seccion, ruta, tamano)
if "saves" in sections:
for r in _save_roots(f):
- todo += [("saves", p, s) for p, s in _ftp_walk(f, r, [], BK_CANCEL)]
+ todo += [("saves", p, s) for p, s in _ftp_walk_sizes(f, r, [], BK_CANCEL)]
if "config" in sections:
for r in BK_CONFIG_DIRS:
- todo += [("config", p, s) for p, s in _ftp_walk(f, r, [], BK_CANCEL, BK_MAX_FILE)]
+ todo += [("config", p, s) for p, s in _ftp_walk_sizes(f, r, [], BK_CANCEL, BK_MAX_FILE)]
if "appdb" in sections:
for p in BK_APPDB:
try:
@@ -5461,7 +5552,7 @@ def health_check():
# Actualizaciones: desde un .zip o desde GitHub
# ---------------------------------------------------------------------------
-UPDATE_EXTS = (".py", ".html", ".txt", ".bat", ".vbs", ".ico", ".js", ".json", ".png", ".css", ".md", ".svg")
+UPDATE_EXTS = (".py", ".html", ".txt", ".bat", ".vbs", ".sh", ".ico", ".js", ".json", ".png", ".css", ".md", ".svg")
def _ver_tuple(v):
@@ -5598,6 +5689,8 @@ def install_zip(data):
with open(tmp, "wb") as fo:
fo.write(z.read(n))
os.replace(tmp, dst)
+ if dst.endswith(".sh") and os.name != "nt":
+ os.chmod(dst, 0o755)
return new_ver
@@ -5608,6 +5701,8 @@ def restart_soon():
kw = {"cwd": HERE}
if os.name == "nt":
kw["creationflags"] = 0x00000008 | 0x00000200 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
+ else:
+ kw["start_new_session"] = True
def go():
time.sleep(0.8)
try:
@@ -5748,7 +5843,7 @@ class Handler(BaseHTTPRequestHandler):
if not token_ok((q.get("t") or [""])[0]):
return self._send(403, {"ok": False})
try:
- fp, ctype = media_cached((q.get("p") or [""])[0])
+ fp, ctype = media_cached((q.get("p") or [""])[0], preview=bool((q.get("v") or [""])[0]))
except Exception as e:
return self._send(404, {"ok": False, "error": str(e)})
size = os.path.getsize(fp)
@@ -5826,7 +5921,8 @@ class Handler(BaseHTTPRequestHandler):
"unread": sum(1 for n in NOTIFS if not n.get("read")), "settings": dict(SETTINGS), "jobs": jobs,
"inbox": len(INBOX), "repo": SETTINGS.get("update_repo") or DEFAULT_REPO,
"sched": {"start": SCHED["start"], "after": SCHED["after"], "countdown": SCHED["countdown"]},
- "lan": bool(LAN["srv"]), "remote": self.lan, "os": os.name}
+ "lan": bool(LAN["srv"]), "remote": self.lan, "os": os.name,
+ "platform": sys.platform}
st["settings"].pop("ftp_pass", None)
if self.lan:
st["settings"].pop("lan_token", None)
@@ -5885,8 +5981,7 @@ class Handler(BaseHTTPRequestHandler):
if p == "/api/open_ext_folder":
folder = os.path.join(HERE, "extension")
try:
- if os.name == "nt":
- os.startfile(folder)
+ F.open_path(folder)
return self._send(200, {"ok": True, "path": folder})
except Exception as e:
return self._send(200, {"ok": False, "path": folder, "error": str(e)})
@@ -6440,15 +6535,26 @@ def forward_to_running(ses, paths):
def _find_app_browser():
- """Edge (viene con Windows) o Chrome, para abrir la app en su propia ventana."""
- if os.name != "nt":
- return None
+ """Edge o Chrome (o Chromium/Brave), para abrir la app en su propia ventana sin pestañas."""
+ import shutil
cands = []
- for env in ("PROGRAMFILES(X86)", "PROGRAMFILES", "LOCALAPPDATA"):
- root = os.environ.get(env)
- if root:
- cands.append(os.path.join(root, "Microsoft", "Edge", "Application", "msedge.exe"))
- cands.append(os.path.join(root, "Google", "Chrome", "Application", "chrome.exe"))
+ if os.name == "nt":
+ for env in ("PROGRAMFILES(X86)", "PROGRAMFILES", "LOCALAPPDATA"):
+ root = os.environ.get(env)
+ if root:
+ cands.append(os.path.join(root, "Microsoft", "Edge", "Application", "msedge.exe"))
+ cands.append(os.path.join(root, "Google", "Chrome", "Application", "chrome.exe"))
+ elif sys.platform == "darwin":
+ for base in ("/Applications", os.path.expanduser("~/Applications")):
+ for app, exe in (("Google Chrome", "Google Chrome"), ("Microsoft Edge", "Microsoft Edge"),
+ ("Brave Browser", "Brave Browser"), ("Chromium", "Chromium")):
+ cands.append(os.path.join(base, app + ".app", "Contents", "MacOS", exe))
+ else:
+ for n in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge",
+ "microsoft-edge-stable", "brave-browser", "brave"):
+ w = shutil.which(n)
+ if w:
+ cands.append(w)
for c in cands:
if os.path.isfile(c):
return c
@@ -6567,6 +6673,16 @@ def taskbar_watch():
def focus_window():
"""Si la ventana ya esta abierta, la trae al frente (en vez de abrir otra). True si habia."""
+ if os.name != "nt":
+ import shutil
+ import subprocess
+ if sys.platform.startswith("linux") and shutil.which("wmctrl"):
+ try:
+ return subprocess.run(["wmctrl", "-x", "-a", "PS5Directo"], capture_output=True,
+ timeout=5).returncode == 0
+ except Exception:
+ return False
+ return False
try:
import ctypes
u32 = ctypes.windll.user32
@@ -6589,9 +6705,12 @@ def open_window(url):
import subprocess
profile = os.path.join(DATA_DIR, "ventana")
try:
- return subprocess.Popen([exe, "--app=" + url, "--user-data-dir=" + profile,
- "--window-size=1150,900", "--no-first-run",
- "--no-default-browser-check"])
+ args = [exe, "--app=" + url, "--user-data-dir=" + profile, "--window-size=1150,900",
+ "--no-first-run", "--no-default-browser-check"]
+ if os.name != "nt" and sys.platform != "darwin":
+ args.append("--class=PS5Directo") # en Linux agrupa la ventana con su icono del menu
+ return subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, start_new_session=os.name != "nt")
except Exception:
pass
webbrowser.open(url)
@@ -6605,9 +6724,41 @@ def _powershell(script, timeout=30):
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
+def ensure_linux_menu():
+ """Linux: entrada "PS5 Directo" en el menu de aplicaciones (con su icono). Se rehace si el
+ programa cambia de carpeta. Tambien acepta archivos soltados encima (Exec ... %F)."""
+ launcher = os.path.join(HERE, "ps5directo.sh")
+ if not os.path.isfile(launcher):
+ return
+ try:
+ if not os.access(launcher, os.X_OK):
+ os.chmod(launcher, 0o755)
+ apps = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "applications")
+ fp = os.path.join(apps, "ps5directo.desktop")
+ q = lambda t: '"' + t.replace("\\", "\\\\").replace('"', '\\"') + '"'
+ body = ("[Desktop Entry]\nType=Application\nName=PS5 Directo\n"
+ "Comment=Descarga, convierte y envía a tu PS5\nComment[en]=Download, convert and send to your PS5\n"
+ "Exec=%s %%F\nIcon=%s\nTerminal=false\nCategories=Network;FileTransfer;\n"
+ "StartupWMClass=PS5Directo\n" % (q(launcher), os.path.join(HERE, "web", "icon-256.png")))
+ try:
+ with open(fp, encoding="utf-8") as f:
+ if f.read() == body:
+ return
+ except OSError:
+ pass
+ os.makedirs(apps, exist_ok=True)
+ with open(fp, "w", encoding="utf-8") as f:
+ f.write(body)
+ print("Entrada en el menu de aplicaciones:", fp)
+ except Exception as e:
+ print("No se pudo crear la entrada del menu:", e)
+
+
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 sys.platform.startswith("linux"):
+ return ensure_linux_menu()
if os.name != "nt":
return
try:
diff --git a/features.py b/features.py
index 0a2f469..4d7bf9e 100644
--- a/features.py
+++ b/features.py
@@ -1211,13 +1211,41 @@ def diagnose(ps5_ip, cfg, speed=None, active=0):
# Energia: no dormir mientras hay trabajo y apagar/suspender al acabar
# ---------------------------------------------------------------------------
+_AWAKE = {"proc": None}
+
+
def keep_awake(on):
- if os.name != "nt":
+ """Que el PC no se suspenda mientras hay envios. En Linux/Mac se hace con un proceso
+ (systemd-inhibit / caffeinate) que dura mientras haga falta."""
+ if os.name == "nt":
+ try:
+ import ctypes
+ ES_CONTINUOUS, ES_SYSTEM_REQUIRED = 0x80000000, 0x00000001
+ ctypes.windll.kernel32.SetThreadExecutionState(ES_CONTINUOUS | (ES_SYSTEM_REQUIRED if on else 0))
+ return True
+ except Exception:
+ return False
+ p = _AWAKE["proc"]
+ if not on:
+ if p is not None:
+ try:
+ p.terminate()
+ except Exception:
+ pass
+ _AWAKE["proc"] = None
+ return True
+ if p is not None and p.poll() is None:
+ return True
+ if sys.platform == "darwin":
+ cmd = ["caffeinate", "-i", "-w", str(os.getpid())]
+ elif shutil.which("systemd-inhibit"):
+ cmd = ["systemd-inhibit", "--what=sleep:idle", "--who=PS5 Directo", "--why=Enviando a la PS5",
+ "--mode=block", "sleep", "infinity"]
+ else:
return False
try:
- import ctypes
- ES_CONTINUOUS, ES_SYSTEM_REQUIRED = 0x80000000, 0x00000001
- ctypes.windll.kernel32.SetThreadExecutionState(ES_CONTINUOUS | (ES_SYSTEM_REQUIRED if on else 0))
+ _AWAKE["proc"] = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
return True
except Exception:
return False
@@ -1225,15 +1253,73 @@ def keep_awake(on):
def power_action(kind):
"""'sleep' (suspender) o 'shutdown' (apagar). Devuelve un texto de lo hecho."""
- if os.name != "nt":
- return "Solo funciona en Windows"
- if kind == "shutdown":
- subprocess.Popen(["shutdown", "/s", "/t", "0", "/c", "PS5 Directo: cola terminada"], creationflags=NO_WINDOW)
- return "Apagando el PC"
- if kind == "sleep":
- subprocess.Popen(["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"], creationflags=NO_WINDOW)
- return "Suspendiendo el PC"
- return ""
+ if os.name == "nt":
+ if kind == "shutdown":
+ subprocess.Popen(["shutdown", "/s", "/t", "0", "/c", "PS5 Directo: cola terminada"], creationflags=NO_WINDOW)
+ return "Apagando el PC"
+ if kind == "sleep":
+ subprocess.Popen(["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"], creationflags=NO_WINDOW)
+ return "Suspendiendo el PC"
+ return ""
+ keep_awake(False)
+ if sys.platform == "darwin":
+ cmd = {"shutdown": ["osascript", "-e", 'tell application "System Events" to shut down'],
+ "sleep": ["pmset", "sleepnow"]}.get(kind)
+ else:
+ cmd = {"shutdown": ["systemctl", "poweroff"], "sleep": ["systemctl", "suspend"]}.get(kind)
+ if not cmd:
+ return ""
+ try:
+ subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ except Exception as e:
+ return "No pude: %s" % e
+ return "Apagando el PC" if kind == "shutdown" else "Suspendiendo el PC"
+
+
+def open_path(path):
+ """Abre una carpeta o archivo del PC con el programa del sistema (Explorador, Finder...)."""
+ if os.name == "nt":
+ os.startfile(path)
+ return
+ cmd = ["open", path] if sys.platform == "darwin" else ["xdg-open", path]
+ subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+
+def pick_native(kind, title):
+ """Selector de archivos/carpeta del sistema en Linux (zenity/kdialog) o Mac (osascript).
+ Devuelve la lista elegida ([] si se cancela) o None si no hay ninguno disponible."""
+ title = title or "PS5 Directo"
+ if sys.platform == "darwin":
+ if kind == "folder":
+ script = 'POSIX path of (choose folder with prompt %s)' % _as_str(title)
+ else:
+ script = ('set l to choose file with prompt %s with multiple selections allowed\n'
+ 'set o to ""\nrepeat with f in l\nset o to o & POSIX path of f & linefeed\nend repeat\nreturn o'
+ % _as_str(title))
+ args = ["osascript"]
+ for ln in script.split("\n"):
+ args += ["-e", ln]
+ p = subprocess.run(args, capture_output=True, timeout=3600)
+ if p.returncode != 0:
+ return []
+ return [x.rstrip("/") or "/" for x in p.stdout.decode("utf-8", "replace").splitlines() if x.strip()]
+ if shutil.which("zenity"):
+ cmd = ["zenity", "--file-selection", "--title=" + title, "--separator=\n"]
+ cmd += ["--directory"] if kind == "folder" else ["--multiple"]
+ elif shutil.which("kdialog"):
+ home = os.path.expanduser("~")
+ cmd = (["kdialog", "--title", title, "--getexistingdirectory", home] if kind == "folder" else
+ ["kdialog", "--title", title, "--getopenfilename", home, "--multiple", "--separate-output"])
+ else:
+ return None
+ p = subprocess.run(cmd, capture_output=True, timeout=3600)
+ if p.returncode != 0:
+ return []
+ return [x for x in p.stdout.decode("utf-8", "replace").splitlines() if x.strip()]
+
+
+def _as_str(t):
+ return '"' + str(t).replace("\\", "\\\\").replace('"', '\\"') + '"'
# ---------------------------------------------------------------------------
diff --git a/installer/macos/Info.plist b/installer/macos/Info.plist
new file mode 100644
index 0000000..0615c03
--- /dev/null
+++ b/installer/macos/Info.plist
@@ -0,0 +1,18 @@
+
+
+
+
+ CFBundleNamePS5 Directo
+ CFBundleDisplayNamePS5 Directo
+ CFBundleIdentifierio.github.jstrdev.ps5directo
+ CFBundleExecutablePS5 Directo
+ CFBundleIconFileps5directo
+ CFBundlePackageTypeAPPL
+ CFBundleShortVersionString__VERSION__
+ CFBundleVersion__VERSION__
+ CFBundleInfoDictionaryVersion6.0
+ LSMinimumSystemVersion11.0
+ NSHighResolutionCapable
+ LSApplicationCategoryTypepublic.app-category.utilities
+
+
diff --git a/installer/macos/LEEME-Mac.txt b/installer/macos/LEEME-Mac.txt
new file mode 100644
index 0000000..a8b1c8a
--- /dev/null
+++ b/installer/macos/LEEME-Mac.txt
@@ -0,0 +1,64 @@
+PS5 DIRECTO PARA MAC (EN PRUEBAS)
+=================================
+
+Esta versión todavía no se ha podido probar en un Mac de verdad. Si algo
+falla, en el programa pulsa "Crear informe de fallo" y adjúntalo en un
+aviso en GitHub: https://github.com/jstrdev/ps5directo/issues
+Con eso se arregla rápido. ¡Gracias por probarla!
+
+INSTALAR
+1. Arrastra "PS5 Directo" a la carpeta Aplicaciones (Applications).
+2. Ábrelo desde Aplicaciones.
+
+SI MACOS NO TE DEJA ABRIRLO
+La app no está firmada por Apple (eso cuesta 99 $ al año), así que la
+primera vez macOS la bloquea. Es normal en programas gratuitos.
+- macOS 15 (Sequoia) o posterior: intenta abrirla, cierra el aviso y ve a
+ Ajustes del Sistema > Privacidad y seguridad. Abajo verás "Se ha
+ bloqueado PS5 Directo": pulsa "Abrir igualmente".
+- macOS 14 o anterior: clic derecho (o Control + clic) sobre la app >
+ Abrir > Abrir.
+- Si aun así no abre, en Terminal:
+ xattr -dr com.apple.quarantine "/Applications/PS5 Directo.app"
+
+NOTAS
+- Funciona en Mac con Apple Silicon (M1, M2...) e Intel, macOS 11 o
+ posterior. Trae Python y 7-Zip.
+- Para la ventana sin pestañas usa Chrome, Edge o Brave si los tienes; si
+ no, se abre en Safari.
+- Para los torrents hace falta aria2: instala Homebrew (brew.sh) y luego
+ "brew install aria2".
+- Se actualiza solo desde el propio programa, como en Windows.
+
+
+PS5 DIRECTO FOR MAC (TESTING)
+=============================
+
+This version hasn't been tested on a real Mac yet. If something fails,
+click "Create problem report" in the program and attach it to an issue
+on GitHub: https://github.com/jstrdev/ps5directo/issues
+That makes it quick to fix. Thanks for trying it!
+
+INSTALL
+1. Drag "PS5 Directo" into the Applications folder.
+2. Open it from Applications.
+
+IF MACOS WON'T OPEN IT
+The app isn't signed by Apple (that costs $99 a year), so macOS blocks it
+the first time. That's normal for free programs.
+- macOS 15 (Sequoia) or later: try to open it, close the warning and go
+ to System Settings > Privacy & Security. Near the bottom you'll see
+ "PS5 Directo was blocked": click "Open Anyway".
+- macOS 14 or earlier: right-click (or Control-click) the app > Open >
+ Open.
+- If it still won't open, in Terminal:
+ xattr -dr com.apple.quarantine "/Applications/PS5 Directo.app"
+
+NOTES
+- Works on Apple Silicon (M1, M2...) and Intel Macs, macOS 11 or later.
+ Python and 7-Zip are included.
+- For the window without tabs it uses Chrome, Edge or Brave if you have
+ one; otherwise it opens in Safari.
+- Torrents need aria2: install Homebrew (brew.sh), then
+ "brew install aria2".
+- It updates itself from the program, like on Windows.
diff --git a/installer/macos/launcher.sh b/installer/macos/launcher.sh
new file mode 100644
index 0000000..20deabd
--- /dev/null
+++ b/installer/macos/launcher.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+# Lanzador de "PS5 Directo.app" (va en Contents/MacOS/PS5 Directo).
+# - Python incluido para Apple Silicon (python-arm64) e Intel (python-x86_64), en Contents/Resources.
+# - El programa se copia a ~/Library/Application Support/PS5 Directo/app y se ejecuta desde ahi,
+# para que «Actualizar» pueda escribir aunque la app este en /Applications. Si la app trae una
+# version mas nueva que la copiada, se vuelve a copiar.
+RES="$(cd "$(dirname "$0")/../Resources" && pwd)"
+SUP="$HOME/Library/Application Support/PS5 Directo"
+APP="$SUP/app"
+LOG="$SUP/lanzador.log"
+mkdir -p "$SUP"
+
+case "$(uname -m)" in
+ arm64) PY="$RES/python-arm64/bin/python3" ;;
+ *) PY="$RES/python-x86_64/bin/python3" ;;
+esac
+[ -x "$PY" ] || PY="$RES/python-x86_64/bin/python3" # Rosetta, por si acaso
+
+ver() { sed -n 's/^VERSION = "\([^"]*\)".*/\1/p' "$1" 2>/dev/null | head -1; }
+newer() { # 0 si $1 es mas nueva que $2
+ [ -z "$2" ] && return 0
+ [ "$1" = "$2" ] && return 1
+ [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)" = "$1" ]
+}
+BUNDLED="$(ver "$RES/app/app.py")"
+COPIED="$(ver "$APP/app.py")"
+if [ ! -f "$APP/app.py" ] || newer "$BUNDLED" "$COPIED"; then
+ echo "$(date) copio la version $BUNDLED (habia: ${COPIED:-ninguna})" >> "$LOG"
+ rm -rf "$APP.nuevo"
+ cp -R "$RES/app" "$APP.nuevo" && rm -rf "$APP" && mv "$APP.nuevo" "$APP"
+fi
+
+export PATH="$RES/bin:$PATH" # 7-Zip incluido
+export PYTHONUNBUFFERED=1
+cd "$APP" || exit 1
+if [ ! -x "$PY" ]; then
+ osascript -e 'display alert "PS5 Directo" message "No encuentro el Python incluido en la app. Vuelve a descargarla de github.com/jstrdev/ps5directo/releases"' >/dev/null 2>&1
+ exit 1
+fi
+mkdir -p "$HOME/.ps5directo"
+# El programa sigue en segundo plano y el lanzador termina: asi cada clic en el icono vuelve a
+# entrar aqui y, si ya estaba abierto, solo trae su ventana (si no, macOS creeria que la app
+# sigue abierta y no haria nada).
+nohup "$PY" "$APP/app.py" "$@" >> "$HOME/.ps5directo/ps5directo.log" 2>&1 < /dev/null &
+exit 0
diff --git a/ps5directo.sh b/ps5directo.sh
new file mode 100644
index 0000000..fe7e79a
--- /dev/null
+++ b/ps5directo.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+# Abre PS5 Directo en Linux (y en Mac, si lo usas desde la carpeta).
+# Usa el Python que viene incluido (carpeta python/) o, si no esta, el python3 del sistema.
+# Puedes pasarle archivos: ./ps5directo.sh juego.rar
+DIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$DIR" || exit 1
+# 7-Zip incluido (carpeta bin/), para "Descomprimir en el PC"
+[ -d "$DIR/bin" ] && PATH="$DIR/bin:$PATH" && export PATH
+if [ -x "$DIR/python/bin/python3" ]; then
+ PY="$DIR/python/bin/python3"
+elif command -v python3 >/dev/null 2>&1; then
+ PY="python3"
+else
+ MSG="No encuentro Python 3. Instálalo (por ejemplo: sudo apt install python3) o baja la versión de PS5 Directo para Linux, que ya lo trae."
+ command -v zenity >/dev/null 2>&1 && zenity --error --title="PS5 Directo" --text="$MSG" 2>/dev/null
+ echo "$MSG" >&2
+ exit 1
+fi
+export PYTHONUNBUFFERED=1
+# Abierto desde el menu (sin terminal): el registro va al mismo archivo que en Windows
+if [ ! -t 1 ]; then
+ mkdir -p "$HOME/.ps5directo"
+ exec "$PY" "$DIR/app.py" "$@" >> "$HOME/.ps5directo/ps5directo.log" 2>&1
+fi
+exec "$PY" "$DIR/app.py" "$@"
diff --git a/web/index.html b/web/index.html
index be15b1a..38db31a 100644
--- a/web/index.html
+++ b/web/index.html
@@ -325,7 +325,9 @@ tr:hover .dots{opacity:1}
.mdgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-top:10px}
.mdt{position:relative;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--bg);cursor:pointer;aspect-ratio:16/9;display:grid;place-items:center}
.mdt img{width:100%;height:100%;object-fit:cover;display:block}
-.mdt .vd{font-size:34px;color:#cfd8ea}
+.mdt .vd{font-size:34px;color:#fff;position:relative;text-shadow:0 2px 10px rgba(0,0,0,.8)}
+.mdt img.cv{position:absolute;inset:0;opacity:.5}
+.mdt .hb{position:absolute;top:7px;right:7px;font-size:10px;font-weight:700;padding:1px 6px;border-radius:6px;background:rgba(0,0,0,.65);color:#ffd36b;letter-spacing:.04em}
.mdt .nm{position:absolute;left:0;right:0;bottom:0;padding:3px 7px;font-size:11px;background:linear-gradient(transparent,rgba(0,0,0,.75));color:#dfe6f2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mdt input{position:absolute;top:7px;left:7px;width:18px;height:18px}
.mdt.on{outline:2px solid #5b9dff;outline-offset:-2px}
@@ -438,6 +440,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
.su-row>div{flex:1;min-width:0}
.su-legal{font-size:12.5px;color:var(--muted);border-left:3px solid var(--line);padding-left:10px}
@media(max-width:600px){.lib-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}.gact button{font-size:11.5px;padding:4px 5px}}
+body.notwin .winonly{display:none!important}
@@ -479,7 +482,7 @@ header{background:rgba(14,17,22,.97)}.card{background:rgb(22,27,34)}}
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.
+
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.
Las capturas de pantalla y los vídeos guardados en la consola. Pulsa una para verla grande; marca las que quieras y «Bajar al PC» las guarda en Descargas\PS5 Directo.
+
Las capturas de pantalla y los vídeos guardados en la consola. Pulsa una para verla grande; marca las que quieras y «Bajar al PC» las guarda en Descargas › PS5 Directo.
💾 Copias de seguridadTus partidas y ajustes de la consola guardados en el PC por si algo falla.
📱 Móvil y navegadorContrólalo desde el móvil con un QR y manda enlaces desde el navegador sin copiar y pegar.
+
🍎 Versión de Mac en pruebas: todavía no se ha podido probar en un Mac de verdad. Si algo falla, pulsa «Crear informe de fallo» y adjúntalo en un aviso en GitHub (Issues): con eso se arregla rápido. ¡Gracias por probarla!
No es solo un programa para subir archivos: une descarga, conversión, envío y gestión de la consola en uno. Tampoco instala ni cambia nada del sistema: solo mueve los archivos que tú eliges, y cada uno lo usa bajo su responsabilidad.
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).