Settings lists the internal storage and every mounted drive (/mnt/extN,
/mnt/usbN) with its free space. New downloads go to <drive>/patchdl on
the chosen drive and install from there. A title keeps its folder on
the drive where it already has one, so packages downloaded earlier are
still found, listed and installed; while the chosen drive is unplugged,
downloads go to the internal storage. Config and caches stay in
/data/patchdl. Closes#18.
The tile's param.json sets applicationCategoryType 65536, which puts it
in the Media section. The home screen keeps the section a tile was
registered in, so an older tile is uninstalled once and registered
again. Closes#19.
curl hands the body over in ~2 KB chunks, and every pwrite() to /data on
the PS5 costs about the same regardless of size, so downloads topped out
around 8 MB/s. Pieces are now collected in a 2 MB buffer and written in
blocks; the buffer is flushed after a failure or pause too, so resume
keeps what arrived. Only bytes on disk are reported as progress. Download
sockets ask for a 4 MB receive buffer (the console grants 512 KB).
Measured by the reporter on FW 13.60 over gigabit: 8.2 -> 116.6 MB/s with
16 connections, 12.5 -> 117.2 MB/s with 4. Closes#17.
Co-authored-by: Nick Mikkelsen <1895436+nikigx2@users.noreply.github.com>
New hero, games, library, settings, phone and social-preview images
with the redesigned cards, a PS4 game and the Other versions list.
The old-firmware screenshot gives way to one of Other versions, and the
highlights mention picking a version, the ten languages and downloads
that get through blocked DNS and dropped connections.
Pausing, a failed job or a restart of PatchDL dropped every piece that
was in flight; with pieces of several GB that is a lot to fetch again.
A stopped piece now syncs what arrived and records it in the sidecar
("have": "piece:bytes,..."), and the next run continues it with a
Range request. Only bytes confirmed by fdatasync are recorded, so a
crash can't make a resume trust data that never reached the disk. The
card and the download list count these bytes, and a restart still offers
Resume for them.
Also:
- Download again during a pause that landed while the manifest was
loading takes the pause back instead of being ignored.
- A failed final flush (disk full) no longer reports a file download
as done.
- Rejected manifests (too many pieces, gaps, empty) report
bad_manifest instead of no reason.
- Cancelling a job that was already cancelling, or one paused while it
was being admitted, freed it while a worker or admit still held it.
- Finished and failed jobs no longer take one of the 16 queue slots, so
'Update all' on a big library doesn't run into queue_full.
- A single package is fetched from scratch, so an old sidecar no longer
marks it complete after it was truncated.
- A shorter package without a sidecar is no longer trusted as a
downloaded prefix.
- Error pages (404, 503) are no longer saved as the .crc file.
- A failed fdatasync no longer counts the piece as done.
- Hash checks fail closed when the digest context can't be allocated.
- JSON buffers stop growing on allocation failure instead of writing
past the old buffer; control characters in names are escaped.
- The diagnostic response is copied instead of pointing at the stack.
- Oversized manifests are rejected before the piece array is allocated,
and a failed fdopen closes its descriptor.
- Network retries now apply to manifests only. The background update
check walks every game and would take minutes longer per game when a
CDN edge drops connections.
- Download workers (256 KB) and the update-list thread (512 KB) get an
explicit stack: the console's default proved too small once, and
curl, OpenSSL and the resolver run on them.
- The saved-addresses rewrite keeps its 16 KB line buffer on the heap.
A piece that failed was fetched again from its first byte. With pieces
of several GB, a connection that drops every few minutes never got one
finished. A retry now asks for the rest with a Range request; with
verification on, the part on disk goes into the hash first (pause and
cancel interrupt that). A server that sends the whole piece instead
overwrites it in place; one that sends another range, or 416, gets the
piece from the start without spending an attempt. A continued piece
that fails its hash is fetched whole once more before the package
counts as corrupt.
Also:
- A reply longer than its piece no longer writes into the next piece.
- Oversized replies and redirect loops are not retried as network errors.
- The diagnosis shows the no-space text for a job that ran out of space.
- README names Quad9 and Orbis Patches.
- Update lists and manifests get two more tries after a network failure
(2 s, 5 s pauses), so one dropped connection no longer fails a
download before its first byte.
- While an aged-out DNS answer is renewed, workers keep using it instead
of waiting for the renewal.
- DNS replies must repeat the question that was asked.
- Waits between piece retries are re-checked every second, so a console
clock set back during a download can't park them.
- A PS5 version without a readable firmware requirement is never offered
as compatible.
- The card says when a download waits to retry after a lost connection.
- A cover whose icon fails falls back to initials without an inline
handler; a malformed #get link no longer throws.
A piece that failed on the network was retried at once, at most four
times, against the same cached address. A short outage or one stalled
CDN edge ended the whole download. Pieces now get up to eight tries
with pauses from 5 s to 60 s, a stall or dropped connection makes the
next request ask DNS again, and each request starts at another address.
An answer only seconds old is kept, so many workers stalling one after
another trigger one lookup, not one each.
The resolver asked one server over UDP and took the first answer, even
a blocker's 0.0.0.0. It now tries UDP on four public resolvers, then
TCP, then DNS over HTTPS (Cloudflare, Google), then the addresses the
last good lookup saved. Sinkhole answers count as blocked, curl gets
every address of a host, and a host whose addresses refuse or drop the
connection is looked up again. Sony hosts stay pinned to Sony's root;
only the DoH requests use public roots.
The network check reports where each answer came from and what failed
before it.
Cards show installed, update and newest version side by side, and
problems as notes with their action. Other versions became a button.
The dialog groups versions into available, needs newer firmware,
installed and older (folded), with larger type throughout. Text is at
least 13px, buttons 44px tall, and every control has a focus ring.
Phones get two status tiles per row and full-width card content.
A PS4 patch that fits in one piece is listed by Orbis as "<name>.pkg"
without a number; its manifest is "<name>.json" in the same gs2 folder,
like split packages. A wrong link on a PS4 game now explains Orbis' "Copy
all links!" instead of asking for Prospero's SC link.
All translations were reviewed against per-language glossaries (PS5 and
common app wording): e.g. German "Herunterladen" instead of "Holen",
status texts as "Wird ... ", requirements as "erfordert FW".
The UI follows the browser language, which on the PS5 is the system
language, and Settings has a selector to override it per device (kept in
localStorage). All UI text moved into web/i18n.js (306 keys) with English,
German, French, Spanish, Italian, Portuguese (Brazil), Japanese, Simplified
Chinese, Korean and Russian; missing keys fall back to English.
Log entries keep their key and values, so the Logs view shows them in the
chosen language while the diagnostic report stays English for issues.
An optional "Other versions" link on a game card lists every version the
patch site knows: Prospero Patches for PS5, its sister site Orbis Patches
for PS4 (same API). Versions newer than the installed one that run on this
firmware can be picked; older ones are shown but not offered, since
installing an older patch over a newer one is untested.
Picking a version opens the existing guide for exactly that version: QR
code to the phone, copy the link on the site, paste it. Orbis offers the
piece links of a PS4 patch ("Copy all links!"); they sit in the same gs2
folder as the manifest, so PatchDL derives "<name>.json" from any
"<name>_<n>.pkg" and fetches it over http like other PS4 manifests. The
size check uses the size the list showed for the version picked.
The Prospero lookup behind "Old FW patch" now uses the same list.
Sideloaded apps
- A Sony-signed package installed with another tool ("fake": true in
app.json, but no PatchDL loopback URL) stays Official but is marked
Sideloaded, and its Updates switch starts off, so Update all skips it.
Switching it on is saved separately, so a switch saved earlier can't
turn it on. The card says an update replaces this exact version.
Failed downloads
- When the package list can't be fetched, the download job records why
(DNS, connection, timeout, TLS, redirect, HTTP status, not a manifest)
with the curl and HTTP codes; the card says so with a Details link.
- Details runs /api/netcheck?title=<id>, which walks the game's update
list and manifest the way a download does and reports PatchDL's own
DNS answer for each host. The dialog shows a checklist and a report to
copy into an issue; on the PS5 a QR code opens it on a phone.
PS4 patches install on the PS5 through the same loopback route as PS5
updates; the console puts them in /user/patch/<id>/ next to the game.
Tested with Bloodborne (CUSA00207, 01.00 to 01.09) on FW 11.60.
- Installed version: PS4 titles have no CONTENT_VERSION in app.db; use
APP_VER, which includes an installed patch.
- Update check: Sony lists PS4 patches at a gs-sec URL derived from the
title id with HMAC-SHA256 (the well-known PS4 update key). A PS4
package's system_ver is a PS4 firmware in decimal and never blocks it.
- Download: PS4 manifests are served over plain http on gs2 only (its
https edge has no Sony certificate); allow http for that host's
manifests, keep everything else https. Piece hashes are SHA-1 there, so
verification picks SHA-1 or SHA-256 by length.
- After an install, check /user/patch/<id>/patch.json instead of app.json.
PS4 installs fetch no .crc, so a minute of quiet after reading ends one.
- The UI shows a PS4 badge instead of "not supported".
Also: a list reload no longer ends an install on the card early. The
console writes the new version during the install, and the card briefly
showed Delete before the install poll put it back.
Cards are rebuilt on list updates, and each rebuild restarted the bar's
animation. A negative animation-delay taken from the clock lets a new bar
continue where the old one was. The sweep is linear now instead of easing
in from a standstill, and it stops for prefers-reduced-motion.
Library
- Rescan when /user/app or Sony's app.db changes, after an install, and on
the Refresh button (which also asks Sony for updates again). Later scans
keep a handle to the system root and use relative paths instead of
swapping the process root while other threads run; app.db is copied out
first because SQLite resolves relative paths with getcwd(). The scan
thread gets a 1 MB stack (the default made reads fail with EFAULT).
- Take the newer of app.db and appmeta param.json as installed version:
app.db lags behind until the game is started.
- A package header decides "fake"; the installer's "fake": true marker
only says the package didn't come from Sony's CDN, and PatchDL's own
installs set it too.
- PS4 titles are listed as not supported.
Downloads
- Fetch the manifest's .crc next to the package; the console asks for it.
- Keep the sidecar (with the version) after a download completes, so a
restart still knows the package is whole.
- A package already on disk without a matching sidecar is hashed against
the manifest piece by piece; only pieces that don't match are fetched.
Install refuses an unverified package.
Installs
- One install per title: a second request (another tab, a double click)
gets install_running; an installed version gets already_installed.
Download, Cancel and Delete wait until the console is done reading.
- Progress comes from the bytes PatchDL serves to the console (distinct
ranges), since Sony's status query fails for these installs. Phases:
waiting, reading with percent, finishing, done or stalled. Each title
is tracked on its own.
- After an install: check that the installed version matches and the new
app.json names this package and its size. With "Delete PKG after
install" on, delete the package only then. The setting existed but was
never read; the old cleanup trusted app.db alone and deleted packages
that were never applied.
UI
- Settings list every package on the console with size, version and
whether it is installed, newer or for a game that's gone, with Delete.
The storage tile links there.
- The list reloads quietly when the server reports a change.
- Bigger covers on wide screens, "Updates on/off" on the switch, no
"Shared master" badge, no endless auto-install retries.
sceAppInstUtilAppInstallPkg returns 0 but installs no content: it writes
the new version to app.db, the tile and appmeta, while the game keeps
running the old one. That is what #9 and #11 report (version shows new,
game unchanged, install stuck at 0%).
InstallByPackage had been refused with 0x80B2116F because MetaInfo was the
published 48-byte struct. Sony reads past it; the stack bytes there sent
the install down another path. MetaInfo is now 0x70 bytes and fully zeroed.
- Try the package over http://127.0.0.1:12880/api/pkg first, so the console
streams it from PatchDL, then the LAN address, the /user/data path and
file://. content_id stays empty.
- Drop the AppInstallPkg fallback: a refusal beats a fake success.
- Wait up to 15 s for the install backend instead of failing the first
click with "install backend not ready".
Verified on a PS5 (FW 11.60) with Dead Island 2, Ghostwire: Tokyo and
Atlas Fallen: app.pkg replaced, packageDigest equal to Sony's manifest,
games start on the new version.
Since the version line rework, "Newest … · needs FW …" only appeared when
the newest patch was newer than the installed one, so up-to-date games,
fake packages and Shadowmount titles lost the firmware information that
0.0.8 showed. It is back whenever Sony reports a newest version, unless
the Update entry already names the same version.
/api/pkgmeta now calls a package installable when its embedded title id
matches the installed game, which is what AppInstallPkg goes by, instead
of flagging every shared-master package. The unused UI message for the
old refusal is gone, and web/README.md describes the current policy:
Shadowmount, unknown and fake titles get neither download nor install,
and shared-master packages install like any other.
The UI hid Install for games whose patch pieces live under another
region's title id. That block predates the switch to AppInstallPkg on the
manifest-assembled package, which carries the installed game's own
content id; Dead Island 2 (EU disc, US master pieces) updated fine that
way. The server has a branch for exactly this case, so the UI now offers
Install again and only keeps the "Shared master" label.
A mounted image can't take a patch, so a download leads nowhere. The
server now refuses downloads and Prospero links for Shadowmount titles
and skips the Prospero lookup for them; the UI lists them under Not
supported without a button, the same as fake packages.
Shadowmount titles can't take a patch, so they no longer sit under Updates.
They move to Not supported and keep their download: Download for Sony
updates, Old FW download for patches from Prospero. Fake packages stay
without any download.
Prospero's human check usually passes without the user noticing, so the
guide now only asks to confirm it if one shows up.
Disc and store installs whose update Sony serves from another region's
master package can't be installed from homebrew; Sony's installer refused
them in testing. They now sit under Not supported with a short reason
instead of offering a download that leads nowhere.
Before preallocating a package, PatchDL now checks that it fits on /data
with 2 GB to spare and reports how much it needs. The Prospero link check
runs the same test, so the phone gets the answer right away.
The PS5 browser can't copy links, so on the console the old-FW guide only
shows a large QR code and the link for the phone.
Sony's version.xml only lists the newest patch. When that one needs a
newer firmware, PatchDL now asks Prospero Patches for the version history
and offers the newest patch that still runs on the console. Prospero only
hands out download links after a human check, so a guide with a QR code
moves the user to a phone, where they copy the SC link. PatchDL derives
Sony's manifest from that link and downloads like an official update.
The game list now groups by what the user can do: Updates, Up to date,
Not supported. Update all read a nonexistent state.games and did nothing;
it now uses state.titles.
New screenshots for the hero, games, queue, settings and phone views,
all with game icons. Add a library shot sorted A to Z, mention the
Needs FW label and the package check in the highlights, and answer
which games PatchDL updates in the FAQ.
- The scan opens each game's icon0.png (app.db icon0Info first, then
/user/appmeta and the title's sce_sys) while it can still see the
whole filesystem, and keeps the fd. GET /api/icon/<title_id> serves
it with a one-day cache. Cards show the icon and fall back to the
initials; loaded icons are reused across re-renders.
- "Latest x · FW y" read like the console firmware. It now says
"needs FW y".
The update check only reads Sony's version.xml, so it is safe for fake
packages too. Their cards now show the latest version and the firmware
it needs. Download and install stay blocked.
- Scan only PPSA and CUSA title ids. Homebrew with made-up ids (the
PatchDL tile, loaders, file managers) no longer shows up as a game.
- Installed fake packages carry "fake": true in app.json, the same
marker homebrew uses, so they vanished from the list. Titles with an
app.pkg now stay listed and count as fake when either the marker or
the package header says so.
- Sort the game list by name.
- "Updatable" only holds titles PatchDL can install. Download-only
titles (shadowmounts, shared-master patches) with an update move to
"Can't update" and show "Update · download only".
The scanner treated every title with an app.pkg as official unless its
app.json mentioned a URL. That let fake packages through as official, so
PatchDL offered and tried to install Sony patches on them, while disc and
store installs whose app.json lists CDN pieces ended up as unknown.
Read the app.pkg header instead. A PS5 finalized image (\x7FFIH) is
retail when byte 0x05 has bit 0x80 set; a PS4 container (\x7FCNT) is
retail when bit 0 of byte 4 is set. Retail installs count as official,
fake-signed ones as the new "fake" source, unreadable headers stay
unknown.
Fake packages are listed with a "Not supported" label. PatchDL skips
their update check, and the server refuses download, install and the
enable toggle for them. The titles JSON reports pkg_sign for diagnostics.