6 Commits
Author SHA1 Message Date
Knutwurst bde585eab7 Add a Buy me a coffee link with a QR code to the UI
The sidebar footer and the end of Settings open a dialog with a QR code
for PayPal, so it works from the TV, plus a direct link for phones.
2026-10-16 19:04:03 +02:00
Knutwurst eaab113d8a Add a PayPal donation link to the README and the repo's Sponsor button 2026-10-16 17:42:48 +02:00
Knutwurst c3630000b6 Bump version to 0.2.0 2026-10-16 14:25:40 +02:00
Knutwurst 72b9136ff5 Say PS5 and PS4 games in the README and the UI header 2026-10-16 14:25:39 +02:00
Knutwurst c6f001f857 README: PS4 games are supported 2026-10-16 13:56:41 +02:00
Knutwurst e72c25a56e Update PS4 games too
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.
2026-10-16 13:56:41 +02:00
14 changed files with 220 additions and 47 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
custom: ["https://paypal.me/oliverkoester"]
+17 -3
View File
@@ -5,14 +5,15 @@
</p>
<p align="center">
<b>Official game updates for your PS5, from any browser.</b><br>
PatchDL finds the newest update for every game on your console, downloads it at full speed and installs it.<br>
<b>Official updates for your PS5 and PS4 games, from any browser.</b><br>
PatchDL finds the newest update for every PS5 and PS4 game on your console, downloads it at full speed and installs it.<br>
Run it from your phone, your laptop or the PS5 itself.
</p>
<p align="center">
<img src="https://img.shields.io/github/v/release/knutwurst/ps5-patchdl?style=for-the-badge&label=release&color=22c55e" alt="Latest release">
<img src="https://img.shields.io/github/downloads/knutwurst/ps5-patchdl/total?style=for-the-badge&color=22c55e" alt="Downloads">
<img src="https://img.shields.io/badge/games-PS5%20%7C%20PS4-22c55e?style=for-the-badge" alt="PS5 and PS4 games">
<img src="https://img.shields.io/badge/FW-1.00%E2%80%9313.60-22c55e?style=for-the-badge" alt="Firmware 1.00 to 13.60">
<img src="https://img.shields.io/badge/UI-any%20browser-22c55e?style=for-the-badge" alt="Web UI in any browser">
<img src="https://img.shields.io/github/license/knutwurst/ps5-patchdl?style=for-the-badge&color=3a3a3a" alt="License">
@@ -24,10 +25,17 @@
</a>
</p>
<p align="center">
If you like what I do, you can buy me a coffee*:
<a href="https://paypal.me/oliverkoester"><img src="https://img.shields.io/badge/Donate-PayPal-22c55e?style=flat-square&logo=paypal&logoColor=white" alt="Donate via PayPal"></a><br>
<sub>*It doesn't have to be a generous donation. A few cents are enough to show me who is interested in further development. So the motivation stays and I just know that I am not programming for the bin ;)</sub>
</p>
---
## Highlights
- **PS5 and PS4 games.** PatchDL updates both, straight from Sony's servers. PS4 updates land where the console keeps them, next to the game.
- **Your whole library at a glance.** Every installed game shows up with its icon, its version and the newest update Sony offers for it, sorted A to Z. Install or remove a game and the list follows within seconds, no restart needed.
- **Update all.** One click queues every game that has an update. PatchDL works through the queue and installs each update as soon as it lands.
- **Fast downloads.** Up to 16 parallel connections per update. Change the number in Settings while a download runs.
@@ -79,7 +87,7 @@ Start an update from the couch with your phone, check on it from your laptop, or
No. It fetches updates for games that are already installed on your console. Nothing else.
**Which games does PatchDL update?**
PS5 games you installed from disc or from the PlayStation Store. PatchDL reads each game's package before it offers anything, and it leaves every other title alone. PS4 games are not supported.
PS5 and PS4 games you installed from disc or from the PlayStation Store. PatchDL reads each game's package before it offers anything, and it leaves every other title alone.
**Where do the updates come from?**
From Sony's official update servers, the same files your console downloads on its own. PatchDL downloads only from a short list of PlayStation CDN hosts. To find older patches it also asks [Prospero Patches](https://prosperopatches.com/) which versions exist for a game. That lookup sends the game's title ID and nothing else.
@@ -131,6 +139,12 @@ PS5_HOST=<console-ip> scripts/deploy_ps5.sh
PatchDL is written in C and builds on [libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/), [libcurl](https://curl.se/), [OpenSSL](https://www.openssl.org/), [SQLite](https://sqlite.org/) and the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk). The version history behind older patches comes from [Prospero Patches](https://prosperopatches.com/). Thanks to everyone behind them.
## Support
If you like what I do, you can buy me a coffee*: [![Donate](https://img.shields.io/badge/Donate-PayPal-22c55e?style=flat-square&logo=paypal&logoColor=white)](https://paypal.me/oliverkoester)
<sub>*It doesn't have to be a generous donation. A few cents are enough to show me who is interested in further development. So the motivation stays and I just know that I am not programming for the bin ;)</sub>
## License
PatchDL is free software under the **GNU General Public License v3.0 or later**. See [LICENSE](LICENSE). Use it, study it, change it, share it. Forks stay under the same license.
+5
View File
@@ -164,6 +164,11 @@ load_db(const char *uri, patchdl_appinfo_t **out, size_t *count) {
if (json) {
json_str(json, "CONTENT_VERSION", e->content_version,
sizeof(e->content_version));
/* PS4 titles carry no CONTENT_VERSION; APP_VER is theirs, and
it includes an installed patch. */
if (!e->content_version[0])
json_str(json, "APP_VER", e->content_version,
sizeof(e->content_version));
json_str(json, "VERSION_FILE_URI", e->version_file_uri,
sizeof(e->version_file_uri));
}
+28 -8
View File
@@ -67,9 +67,16 @@ static const char *ALLOWED_HOSTS[] = {
"sgst.prod.dl.playstation.net",
"gst.prod.dl.playstation.net",
"gs2.ww.prod.dl.playstation.net",
"gs-sec.ww.np.dl.playstation.net", /* PS4 update lists */
NULL,
};
/* PS4 package manifests are published on gs2 over plain http only (https
there is a CloudFront edge without Sony's certificate). The console's own
installer checks the package signature, so a forged manifest can't get an
unsigned package installed. Everything else stays https-only. */
#define PLAIN_HTTP_HOST "gs2.ww.prod.dl.playstation.net"
static int
host_allowed(const char *host) {
size_t hlen = strlen(host);
@@ -88,6 +95,13 @@ host_allowed(const char *host) {
}
/* Extract hostname from https://host/path or http://host/path */
/* Manifest piece hashes: PS5 manifests carry SHA-256 (64 hex digits), PS4
manifests SHA-1 (40). */
static const EVP_MD *
digest_for(const char *hex) {
return (hex && strlen(hex) == 40) ? EVP_sha1() : EVP_sha256();
}
static int
url_host(const char *url, char *out, size_t out_sz) {
const char *p = strstr(url, "://");
@@ -291,8 +305,11 @@ patchdl_http_get(const char *url, patchdl_buf_t *out) {
struct curl_slist *resolve_list = NULL;
struct curl_blob ca_blob;
int plain = !strncmp(url, "http://", 7);
if (url_host(url, host, sizeof(host))) return -1;
if (!host_allowed(host)) return -1;
if (plain && strcasecmp(host, PLAIN_HTTP_HOST)) return -1;
if (dns_lookup(host, ip, sizeof(ip))) return -1;
snprintf(resolve_str, sizeof(resolve_str), "%s:443:%s", host, ip);
@@ -334,8 +351,9 @@ patchdl_http_get(const char *url, patchdl_buf_t *out) {
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
/* Redirects must stay on HTTPS — host_allowed gates the initial URL, but
once libcurl follows a 302 we want the protocol pinned too. The _STR
variants replaced the bitfield options in libcurl 7.85. */
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
variants replaced the bitfield options in libcurl 7.85. Plain http only
for the PS4 manifest host (see PLAIN_HTTP_HOST). */
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, plain ? "http" : "https");
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "patchdl/1.0");
@@ -517,7 +535,7 @@ http_download_to_file_progress(const char *url, FILE *fp, long long *bytes_out,
if (verify) {
sink.md = EVP_MD_CTX_new();
if (sink.md)
EVP_DigestInit_ex(sink.md, EVP_sha256(), NULL);
EVP_DigestInit_ex(sink.md, digest_for(expected_sha256_hex), NULL);
}
snprintf(rs443, sizeof(rs443), "%s:443:%s", host, ip);
@@ -944,7 +962,7 @@ patchdl_http_download_piece(const char *url, int fd,
sink.bytes_slot = ctx ? ctx->bytes_slot : NULL;
if (verify) {
sink.md = EVP_MD_CTX_new();
if (sink.md) EVP_DigestInit_ex(sink.md, EVP_sha256(), NULL);
if (sink.md) EVP_DigestInit_ex(sink.md, digest_for(expected_sha256_or_null), NULL);
}
snprintf(rs443, sizeof(rs443), "%s:443:%s", host, ip);
@@ -1018,10 +1036,12 @@ patchdl_http_download_piece(const char *url, int fd,
return 0;
}
/* Read-only: SHA-256 a [offset, offset+size) region of fd into out_hex (>=65
bytes). Uses pread so it doesn't disturb the fd offset. 0 on success. */
/* Read-only: hash a [offset, offset+size) region of fd into out_hex (>=65
bytes), with the algorithm `like_hex` is written in (see digest_for). Uses
pread so it doesn't disturb the fd offset. 0 on success. */
int
patchdl_sha256_fd_region(int fd, long long offset, long long size, char *out_hex) {
patchdl_hash_fd_region(int fd, long long offset, long long size,
const char *like_hex, char *out_hex) {
EVP_MD_CTX *md;
unsigned char *buf;
long long pos = offset, remaining = size;
@@ -1033,7 +1053,7 @@ patchdl_sha256_fd_region(int fd, long long offset, long long size, char *out_hex
if (!md) return -1;
buf = malloc(CHUNK);
if (!buf) { EVP_MD_CTX_free(md); return -1; }
EVP_DigestInit_ex(md, EVP_sha256(), NULL);
EVP_DigestInit_ex(md, digest_for(like_hex), NULL);
while (remaining > 0) {
size_t want = remaining > (long long)CHUNK ? CHUNK : (size_t)remaining;
ssize_t got = pread(fd, buf, want, (off_t)pos);
+6 -5
View File
@@ -43,7 +43,7 @@ typedef struct {
char *url;
long long offset; /* byte offset of this piece in the assembled file */
long long size; /* exact length of this piece */
char hash[80]; /* manifest SHA-256 hex, or "" */
char hash[80]; /* manifest hash hex (SHA-256, or SHA-1 for PS4), or "" */
} patchdl_piece_t;
typedef struct {
@@ -79,10 +79,11 @@ int patchdl_http_download_piece(const char *url, int fd,
patchdl_piece_ctx_t *ctx,
int *curl_rc_out, long *http_code_out);
/* Read-only: SHA-256 a [offset, offset+size) region of fd into out_hex
(caller provides >= 65 bytes). Returns 0 on success. */
int patchdl_sha256_fd_region(int fd, long long offset, long long size,
char *out_hex);
/* Read-only: hash a [offset, offset+size) region of fd into out_hex
(caller provides >= 65 bytes), SHA-1 if like_hex has 40 hex digits (PS4
manifests), else SHA-256. Returns 0 on success. */
int patchdl_hash_fd_region(int fd, long long offset, long long size,
const char *like_hex, char *out_hex);
/* Progress callback. Return non-zero to ABORT the in-flight download (used to
cancel large patch downloads); return 0 to continue. */
+34
View File
@@ -9,6 +9,7 @@
#include "patchdl_resolve.h"
#include <openssl/hmac.h>
#include <stdio.h>
#include <string.h>
@@ -52,10 +53,43 @@ lookup_tsv(const char *title_id, char *url_out, size_t url_sz) {
return -1;
}
/* PS4 titles: Sony's update list lives at a path derived from the title id
with HMAC-SHA256 under a key that is part of the PS4 system software and
widely published (it's how every PS4 update checker finds patches). */
static const unsigned char PS4_UPDATE_KEY[32] = {
0xAD, 0x62, 0xE3, 0x7F, 0x90, 0x5E, 0x06, 0xBC, 0x19, 0x59, 0x31, 0x42,
0x28, 0x1C, 0x11, 0x2C, 0xEC, 0x0E, 0x7E, 0xC3, 0xE9, 0x7E, 0xFD, 0xCA,
0xEF, 0xCD, 0xBA, 0xAF, 0xA6, 0x37, 0x8D, 0x84,
};
static int
ps4_update_url(const char *title_id, char *url_out, size_t url_sz) {
unsigned char mac[32];
unsigned int mac_len = 0;
char msg[24], hex[65];
int n;
if (strlen(title_id) != 9) return -1;
n = snprintf(msg, sizeof msg, "np_%s", title_id);
if (!HMAC(EVP_sha256(), PS4_UPDATE_KEY, sizeof PS4_UPDATE_KEY,
(const unsigned char *)msg, (size_t)n, mac, &mac_len) ||
mac_len != sizeof mac)
return -1;
for (unsigned i = 0; i < mac_len; i++)
snprintf(hex + 2 * i, 3, "%02x", mac[i]);
n = snprintf(url_out, url_sz,
"https://gs-sec.ww.np.dl.playstation.net/plo/np/%s/%s/%s-ver.xml",
title_id, hex, title_id);
return (n > 0 && (size_t)n < url_sz) ? 0 : -1;
}
int
patchdl_resolve_url(const char *title_id, char *url_out, size_t url_sz) {
if (!title_id) return -1;
if (!strncmp(title_id, "CUSA", 4))
return ps4_update_url(title_id, url_out, url_sz);
/* Only PS5 native title IDs (PPSA/PPSE/PPSH/PPSJ/PPSC) */
if (strncmp(title_id, "PPSA", 4) &&
strncmp(title_id, "PPSE", 4) &&
+24 -5
View File
@@ -638,15 +638,22 @@ patchdl_scan(patchdl_title_t **titles_out, size_t *count_out) {
return rc;
}
int
patchdl_scan_read_app_file(const char *title_id, const char *name,
char *buf, size_t sz) {
static const char *PATCH_DIRS[] = {
"/user/patch",
"/mnt/ext0/user/patch",
"/mnt/ext1/user/patch",
NULL,
};
static int
read_title_file(const char *const *bases, const char *title_id, const char *name,
char *buf, size_t sz) {
char path[PATH_MAX];
if (g_sysroot_fd < 0 || !sz) return -1;
for (int i = 0; SCAN_DIRS[i]; i++) {
for (int i = 0; bases[i]; i++) {
int fd;
ssize_t n;
snprintf(path, sizeof(path), "%s/%s/%s", SCAN_DIRS[i] + 1, title_id, name);
snprintf(path, sizeof(path), "%s/%s/%s", bases[i] + 1, title_id, name);
fd = openat(g_sysroot_fd, path, O_RDONLY | O_CLOEXEC);
if (fd < 0) continue;
n = read(fd, buf, sz - 1);
@@ -658,6 +665,18 @@ patchdl_scan_read_app_file(const char *title_id, const char *name,
return -1;
}
int
patchdl_scan_read_app_file(const char *title_id, const char *name,
char *buf, size_t sz) {
return read_title_file(SCAN_DIRS, title_id, name, buf, sz);
}
int
patchdl_scan_read_patch_file(const char *title_id, const char *name,
char *buf, size_t sz) {
return read_title_file(PATCH_DIRS, title_id, name, buf, sz);
}
/* Fold one stat result into an FNV-1a hash. */
static unsigned long long
sig_add(unsigned long long h, const char *rel) {
+3
View File
@@ -84,6 +84,9 @@ const char *patchdl_scan_step(void);
0 on success with a NUL-terminated buf. */
int patchdl_scan_read_app_file(const char *title_id, const char *name,
char *buf, size_t sz);
/* Same for an installed PS4 patch: /user/patch/<id>/<name>. */
int patchdl_scan_read_patch_file(const char *title_id, const char *name,
char *buf, size_t sz);
/* Mark scan/debug as no longer safe to call (must be set after MHD worker
threads come up — patchdl_scan performs a process-wide vnode swap that
+1 -1
View File
@@ -9,4 +9,4 @@
#pragma once
#define PATCHDL_VERSION "0.1.0"
#define PATCHDL_VERSION "0.2.0"
+11 -5
View File
@@ -134,20 +134,26 @@ parse_packages(const char *xml, uint32_t fw_bin, patchdl_verinfo_t *out) {
char murl[512] = {0};
/* PS5 version.xml uses content_ver; PS4 uses version. */
if (attr_val(p, tag_end, "content_ver", ver, sizeof(ver)))
int is_ps4 = attr_val(p, tag_end, "content_ver", ver, sizeof(ver)) != 0;
if (is_ps4)
attr_val(p, tag_end, "version", ver, sizeof(ver));
attr_val(p, tag_end, "system_ver", sver, sizeof(sver));
attr_val(p, tag_end, "delta_url", durl, sizeof(durl));
attr_val(p, tag_end, "manifest_url", murl, sizeof(murl));
if (ver[0] && sver[0]) {
uint32_t pkg_sver = parse_hex(sver);
if (ver[0] && (sver[0] || is_ps4)) {
/* A PS4 patch's system_ver is a PS4 firmware (in decimal); the
PS5 runs PS4 games regardless, so it never blocks one. */
uint32_t pkg_sver = is_ps4 ? 0 : parse_hex(sver);
/* Track latest overall */
if (!out->latest_version[0] || ver_gt(ver, out->latest_version)) {
strncpy(out->latest_version, ver, sizeof(out->latest_version) - 1);
fw_bin_to_str(pkg_sver, out->latest_required_fw,
sizeof(out->latest_required_fw));
if (is_ps4)
out->latest_required_fw[0] = '\0';
else
fw_bin_to_str(pkg_sver, out->latest_required_fw,
sizeof(out->latest_required_fw));
}
/* Track latest compatible + its installable patch source. PS5
+16 -16
View File
@@ -145,8 +145,11 @@ stream_find(const char *title_id) {
g_mutex held. */
static int
stream_read_all(const install_stream_t *st) {
int quiet = time(NULL) - st->last >= INSTALL_QUIET_SECS;
return (st->total > 0 && st->read >= st->total) ||
(st->crc && time(NULL) - st->last >= INSTALL_QUIET_SECS);
(st->crc && quiet) ||
/* PS4 patches come without a .crc */
(!strncmp(st->title_id, "CUSA", 4) && st->read > 0 && quiet);
}
#define INSTALL_STALL_SECS 300
@@ -265,7 +268,8 @@ static const char config_tail_json[] =
"\"cdn_allowlist\":["
"\"sgst.prod.dl.playstation.net\","
"\"gst.prod.dl.playstation.net\","
"\"gs2.ww.prod.dl.playstation.net\""
"\"gs2.ww.prod.dl.playstation.net\","
"\"gs-sec.ww.np.dl.playstation.net\""
"]}";
/* ---------- tiny JSON value lookups (flat objects only) ----------------- */
@@ -1104,9 +1108,6 @@ title_status_str(const patchdl_title_t *t) {
if (t->source_type == PATCHDL_SOURCE_UNKNOWN ||
t->source_type == PATCHDL_SOURCE_FAKE ||
t->source_type == PATCHDL_SOURCE_SHADOWMOUNT) return "blocked";
/* PS4 games: their updates live on other Sony servers and install
another way. PatchDL only handles PS5 titles. */
if (!strncmp(t->title_id, "CUSA", 4)) return "blocked";
if (t->compatible_version[0]) {
/* Versions are zero-padded (01.000.011), so strcmp orders them.
A compatible patch newer than what's installed = update available. */
@@ -1879,8 +1880,8 @@ verify_existing(dl_job_t *q, int fd, const patchdl_manifest_t *mf,
pthread_mutex_unlock(&g_pool.mtx);
if (stop) break;
if (mf->pieces[i].hash[0] &&
patchdl_sha256_fd_region(fd, mf->pieces[i].offset, mf->pieces[i].size,
hex) == 0 &&
patchdl_hash_fd_region(fd, mf->pieces[i].offset, mf->pieces[i].size,
mf->pieces[i].hash, hex) == 0 &&
!strcasecmp(hex, mf->pieces[i].hash))
done[i / 8] |= (unsigned char)(1 << (i % 8));
hashed += mf->pieces[i].size;
@@ -2206,9 +2207,6 @@ do_download(struct MHD_Connection *conn, const char *title_id,
if (src == PATCHDL_SOURCE_SHADOWMOUNT)
return queue_json(conn, MHD_HTTP_FORBIDDEN,
"{\"ok\":false,\"reason\":\"source_shadowmount\"}");
if (!strncmp(title_id, "CUSA", 4))
return queue_json(conn, MHD_HTTP_FORBIDDEN,
"{\"ok\":false,\"reason\":\"ps4_not_supported\"}");
if (!patch_url[0])
return queue_json(conn, MHD_HTTP_CONFLICT,
"{\"ok\":false,\"reason\":\"no_compatible_patch\"}");
@@ -2546,9 +2544,7 @@ do_install(struct MHD_Connection *conn, const char *title_id,
if (src != PATCHDL_SOURCE_OFFICIAL && src != PATCHDL_SOURCE_EXTERNAL)
return queue_json(conn, MHD_HTTP_FORBIDDEN,
"{\"ok\":false,\"reason\":\"install_not_allowed_for_source\"}");
if (!strncmp(title_id, "CUSA", 4))
return queue_json(conn, MHD_HTTP_FORBIDDEN,
"{\"ok\":false,\"reason\":\"ps4_not_supported\"}");
if (!patch_url[0] && !delta_url[0])
return queue_json(conn, MHD_HTTP_CONFLICT,
@@ -3325,8 +3321,8 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
char got[80] = {0};
int ok = 0;
if (mf.pieces[i].hash[0] &&
patchdl_sha256_fd_region(fd, mf.pieces[i].offset,
mf.pieces[i].size, got) == 0)
patchdl_hash_fd_region(fd, mf.pieces[i].offset,
mf.pieces[i].size, mf.pieces[i].hash, got) == 0)
ok = (strcasecmp(got, mf.pieces[i].hash) == 0);
if (ok) okc++; else { badc++; if (first_bad < 0) first_bad = i; }
if (i) jbuf_append(&j, ",");
@@ -3556,8 +3552,12 @@ check_one_install(int idx) {
idle = (long long)(time(NULL) - last);
snprintf(size_key, sizeof size_key, "\"originalFileSize\":%lld", total);
aj = malloc(256 * 1024);
/* A PS5 update replaces the game's app.pkg (app.json); a PS4 patch
lands next to it under /user/patch/<id>/ (patch.json). */
ok = aj && version_installed(tid, ver) && file[0] &&
patchdl_scan_read_app_file(tid, "app.json", aj, 256 * 1024) == 0 &&
(strncmp(tid, "CUSA", 4)
? patchdl_scan_read_app_file(tid, "app.json", aj, 256 * 1024)
: patchdl_scan_read_patch_file(tid, "patch.json", aj, 256 * 1024)) == 0 &&
strstr(aj, file) && strstr(aj, size_key);
free(aj);
/* install_reading() would say busy: this install is the one being
+50 -3
View File
@@ -51,6 +51,7 @@ const fallback = {
"sgst.prod.dl.playstation.net",
"gst.prod.dl.playstation.net",
"gs2.ww.prod.dl.playstation.net",
"gs-sec.ww.np.dl.playstation.net",
],
},
titles: [
@@ -121,8 +122,47 @@ document.addEventListener("DOMContentLoaded", () => {
loadInitialData().then(openGuideFromHash);
window.addEventListener("hashchange", openGuideFromHash);
setInterval(watchTitles, 5000);
document.querySelectorAll("[data-donate]").forEach((b) => b.addEventListener("click", openDonate));
});
const DONATE_URL = "https://paypal.me/oliverkoester";
// Donation dialog. On the TV a link is no use, so it shows a QR code for
// the phone (hidden on phones, where the button opens PayPal directly).
function openDonate() {
const back = document.activeElement;
const matrix = qrEncode(DONATE_URL);
const wrap = document.createElement("div");
wrap.className = "guide-backdrop";
wrap.innerHTML = `
<section class="guide" role="dialog" aria-modal="true" aria-labelledby="donateTitle">
<header class="guide-head">
<div>
<p class="eyebrow">Support PatchDL</p>
<h2 id="donateTitle">Buy me a coffee</h2>
</div>
<button class="icon-button guide-close" type="button" aria-label="Close">×</button>
</header>
<p class="guide-why">If you like what I do, you can buy me a coffee.</p>
${matrix ? `<div class="guide-qr">${qrSvg(matrix)}<div>
<span>Scan with your phone to open PayPal:</span>
<code>${escapeHtml(DONATE_URL)}</code></div></div>` : ""}
<a class="row-button is-update donate-open" href="${DONATE_URL}" target="_blank" rel="noopener">Open PayPal</a>
<p class="guide-why"><small>It doesn't have to be a generous donation. A few cents are enough to show me who is interested in further development. So the motivation stays and I just know that I am not programming for the bin ;)</small></p>
</section>`;
const close = () => {
document.removeEventListener("keydown", onKey);
wrap.remove();
if (back && document.contains(back)) back.focus();
};
const onKey = (e) => { if (e.key === "Escape") close(); };
wrap.querySelector(".guide-close").addEventListener("click", close);
wrap.addEventListener("click", (e) => { if (e.target === wrap) close(); });
document.addEventListener("keydown", onKey);
document.body.appendChild(wrap);
wrap.querySelector(".guide-close").focus();
}
// The server rescans on its own when games are added or removed and bumps
// titles_gen whenever the list or an update check changes; reload quietly then.
let watchBusy = false;
@@ -252,9 +292,17 @@ async function loadInitialData({ quiet = false } = {}) {
titles.forEach((g) => {
const old = prev.get(g.title_id);
// Neither state has a patch left to fetch, so drop stale job flags.
// An install ends when the install poll says so, not when the list
// already reports the new version (the console writes it mid-install).
if (old && old.installing) {
g.installing = true;
g.installStatus = old.installStatus;
g.installProgress = old.installProgress;
g.installDone = old.installDone;
g.installTotal = old.installTotal;
}
if (old && g.status !== "up_to_date" &&
(g.status !== "incompatible_fw" || g.manual_version)) {
if (old.installing) g.installing = true;
if (old._autoInstalled) g._autoInstalled = true;
if (old._localDownloading) g._localDownloading = true;
if (old._wasActive) g._wasActive = true;
@@ -476,6 +524,7 @@ function createGameCard(game) {
${game.resumable && !game.downloading ? `<span class="pill warn">Paused</span>` : ""}
${statusPill(game)}
${sourcePill(game)}
${isPs4(game) ? `<span class="pill">PS4</span>` : ""}
</div>
<div class="versions">${versionsHtml(game)}</div>
`;
@@ -616,7 +665,6 @@ function versionsHtml(game) {
function statusPill(game) {
if (game.installing) return `<span class="pill warn">Installing…</span>`;
if (game.status === "checking") return `<span class="pill">Checking…</span>`;
if (isPs4(game)) return `<span class="pill">PS4 · not supported</span>`;
if (game.status === "blocked" || !sourcePolicy(game).allow_install)
return `<span class="pill">Not supported</span>`;
if (game.patch_title_match === false) return `<span class="pill blocked">Title mismatch</span>`;
@@ -1303,7 +1351,6 @@ const REASON_TEXT = {
piece_verify_failed: "A downloaded piece failed its SHA-256 check.",
install_running: "The console is installing from this package right now. Try again when it is done.",
already_installed: "This version is already installed.",
ps4_not_supported: "PS4 games are not supported.",
too_many_installs: "Too many installs running at once. Try again when one has finished.",
download_incomplete: "The download is not complete yet.",
download_unverified: "The package on disk hasn't been checked yet. Press Update: PatchDL verifies it and downloads only what's missing.",
+7 -1
View File
@@ -58,6 +58,7 @@
<div class="rail-foot">
<span id="railFw">FW --</span>
<span id="railSpace">-- free</span>
<button type="button" class="link-button rail-donate" data-donate>Buy me a coffee</button>
</div>
</aside>
@@ -66,7 +67,7 @@
<section class="view is-active" data-view="games">
<header class="topbar">
<div>
<p class="eyebrow">PS5 update manager</p>
<p class="eyebrow">PS5 &amp; PS4 game updates</p>
<h1>Games</h1>
</div>
<div class="topbar-actions">
@@ -239,6 +240,11 @@
<span>Downloads on console <em id="storageTotal"></em></span>
<div id="storageList"></div>
</div>
<div class="donate-row">
<span>Like PatchDL? You can buy me a coffee.</span>
<button type="button" class="row-button is-ghost" data-donate>Buy me a coffee</button>
</div>
</div>
</section>
+17
View File
@@ -116,6 +116,23 @@ svg {
font-size: 12px;
}
.rail-foot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-foot .rail-donate { justify-self: start; margin-top: 4px; }
.donate-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 16px;
padding: 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-2);
color: var(--muted);
}
.donate-row .row-button { width: auto; padding: 0 16px; }
.donate-open { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; justify-self: start; padding: 0 18px; }
/* ---------------- views + topbar ---------------- */