mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-20 16:00:38 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04b63eb800 | ||
|
|
e853bbb0b4 | ||
|
|
e6e7054e95 | ||
|
|
687966bb8e | ||
|
|
4d44427737 |
No files matched your search
@@ -63,7 +63,8 @@ gen/web/%.c: web/% scripts/gen_asset_module.py | gen/web
|
||||
$(SQLITE_OBJ): $(SQLITE_DIR)/sqlite3.c
|
||||
$(CC) $(SQLITE_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BIN): $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ) $(TILE_ASSETS)
|
||||
# Headers too: a changed header (the version string, say) must rebuild.
|
||||
$(BIN): $(SRCS) $(wildcard src/*.h) $(GEN_SRCS) $(SQLITE_OBJ) $(TILE_ASSETS)
|
||||
$(CC) $(CFLAGS) -o $@ $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ) $(LDADD)
|
||||
|
||||
test: $(BIN)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define MAX_TITLES 512
|
||||
@@ -683,6 +684,17 @@ read_title_file(const char *const *bases, const char *title_id, const char *name
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
patchdl_scan_sys_statvfs(const char *path, struct statvfs *out) {
|
||||
int fd, rc;
|
||||
if (g_sysroot_fd < 0 || !path || path[0] != '/') return -1;
|
||||
fd = openat(g_sysroot_fd, path[1] ? path + 1 : ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd < 0) return -1;
|
||||
rc = fstatvfs(fd, out);
|
||||
close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int
|
||||
patchdl_scan_read_app_file(const char *title_id, const char *name,
|
||||
char *buf, size_t sz) {
|
||||
|
||||
@@ -84,6 +84,12 @@ const char *patchdl_scan_step(void);
|
||||
/* Read <title dir>/<name> of an installed title (e.g. app.json) through the
|
||||
kept system root handle; thread-safe, no working-directory change.
|
||||
0 on success with a NUL-terminated buf. */
|
||||
/* statvfs of a path as the system sees it (e.g. a game's /user/app/<id>),
|
||||
through the system root kept by the first scan: the process's own root
|
||||
doesn't show those paths. 0 on success. */
|
||||
struct statvfs;
|
||||
int patchdl_scan_sys_statvfs(const char *path, struct statvfs *out);
|
||||
|
||||
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>. */
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define PATCHDL_VERSION "0.3.2"
|
||||
#define PATCHDL_VERSION "0.3.3"
|
||||
+135
-20
@@ -250,7 +250,8 @@ typedef struct dl_job {
|
||||
int verifying; /* admit is hashing a package found on disk */
|
||||
int in_admit; /* admit_next holds this job with the lock dropped */
|
||||
long long verify_bytes; /* bytes hashed so far */
|
||||
long long free_bytes; /* rc -3: bytes free on /data at the time */
|
||||
long long free_bytes; /* rc -3: bytes free on that drive at the time */
|
||||
int space_install; /* rc -3: short because the install needs room too */
|
||||
int last_curl_rc; /* CURLcode from the most recent piece */
|
||||
char fail_step[24]; /* why admit failed (manifest fetch), or "" */
|
||||
long last_http_code; /* HTTP status from the most recent piece */
|
||||
@@ -954,20 +955,48 @@ queue_pkg_file(struct MHD_Connection *conn, const char *url) {
|
||||
|
||||
/* ---------- status JSON ------------------------------------------------- */
|
||||
|
||||
/* Free space on the drive holding `path`, in MB. A folder that doesn't
|
||||
exist yet counts as its closest existing parent. 0 if unavailable. */
|
||||
/* statvfs of the drive holding `path` (in this process's view). A folder
|
||||
that doesn't exist yet counts as its closest existing parent. */
|
||||
static int
|
||||
statvfs_up(const char *path, struct statvfs *vfs) {
|
||||
char p[288], *slash;
|
||||
snprintf(p, sizeof p, "%s", path);
|
||||
for (;;) {
|
||||
if (statvfs(p, vfs) == 0) return 0;
|
||||
if (!(slash = strrchr(p, '/')) || slash == p) return -1;
|
||||
*slash = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* Free space on the drive holding `path`, in MB. 0 if unavailable. */
|
||||
static long long
|
||||
drive_free_mb(const char *path) {
|
||||
struct statvfs vfs;
|
||||
char p[288], *slash;
|
||||
snprintf(p, sizeof p, "%s", path);
|
||||
for (;;) {
|
||||
if (statvfs(p, &vfs) == 0)
|
||||
return (long long)(((unsigned long long)vfs.f_bavail *
|
||||
(unsigned long long)vfs.f_frsize) / (1024ULL * 1024ULL));
|
||||
if (!(slash = strrchr(p, '/')) || slash == p) return 0;
|
||||
*slash = '\0';
|
||||
}
|
||||
if (statvfs_up(path, &vfs)) return 0;
|
||||
return (long long)(((unsigned long long)vfs.f_bavail *
|
||||
(unsigned long long)vfs.f_frsize) / (1024ULL * 1024ULL));
|
||||
}
|
||||
|
||||
/* Free bytes on a game's drive, -1 if it can't be measured. Game paths
|
||||
(/user/app/<id>, /mnt/extN/user/app/<id>) only resolve through the
|
||||
system root the scanner keeps. */
|
||||
static long long
|
||||
game_free_bytes(const char *game_dir) {
|
||||
struct statvfs vfs;
|
||||
if (!game_dir[0] || patchdl_scan_sys_statvfs(game_dir, &vfs)) return -1;
|
||||
return (long long)((unsigned long long)vfs.f_bavail * (unsigned long long)vfs.f_frsize);
|
||||
}
|
||||
|
||||
/* The download folder and the game sit on the same drive. The two paths
|
||||
live in different views of the filesystem, so compare the file system
|
||||
id, and failing that its exact size. */
|
||||
static int
|
||||
same_drive(const char *dl_dir, const char *game_dir) {
|
||||
struct statvfs a, b;
|
||||
if (!game_dir[0] || statvfs_up(dl_dir, &a) || patchdl_scan_sys_statvfs(game_dir, &b))
|
||||
return 0;
|
||||
if (a.f_fsid && a.f_fsid == b.f_fsid) return 1;
|
||||
return a.f_blocks == b.f_blocks && a.f_frsize == b.f_frsize;
|
||||
}
|
||||
|
||||
/* Free space where new downloads go, in MB. */
|
||||
@@ -983,6 +1012,10 @@ data_free_mb(void) {
|
||||
internal storage to the last byte. */
|
||||
#define PATCHDL_SPACE_MARGIN (2LL * 1024 * 1024 * 1024)
|
||||
|
||||
/* Extra room asked for on the game's drive before an install, on top of the
|
||||
package size. A guess: the installer's exact need isn't known. */
|
||||
#define INSTALL_SPACE_MARGIN (1LL * 1024 * 1024 * 1024)
|
||||
|
||||
/* Bytes still to be written for a package of `total` bytes at `path`: a
|
||||
partial from an earlier run already occupies part of it. */
|
||||
static long long
|
||||
@@ -1002,6 +1035,40 @@ space_ok(const char *dir, long long need, long long *free_out) {
|
||||
return need + PATCHDL_SPACE_MARGIN <= free_b;
|
||||
}
|
||||
|
||||
/* Is there room to download what's missing of a package and then install
|
||||
it? The install needs about the package's size again on the game's drive
|
||||
(game_dir); on the drive of the download folder (dl_dir) the two share
|
||||
the free space. 1 if it fits. Otherwise *need_out / *free_out describe
|
||||
the drive that is short and *for_install says whether the install part
|
||||
made it short. */
|
||||
static int
|
||||
download_and_install_fit(const char *dl_dir, const char *game_dir,
|
||||
long long remaining, long long total,
|
||||
long long *need_out, long long *free_out, int *for_install) {
|
||||
long long inst = total + INSTALL_SPACE_MARGIN, free_b = 0;
|
||||
int same = same_drive(dl_dir, game_dir);
|
||||
|
||||
*for_install = 0;
|
||||
if (!space_ok(dl_dir, remaining + (same ? inst : 0), &free_b)) {
|
||||
*need_out = remaining + (same ? inst : 0);
|
||||
*free_out = free_b;
|
||||
*for_install = same;
|
||||
return 0;
|
||||
}
|
||||
if (game_dir[0] && !same) {
|
||||
long long gfree = game_free_bytes(game_dir);
|
||||
if (gfree >= 0 && gfree < inst) {
|
||||
*need_out = inst;
|
||||
*free_out = gfree;
|
||||
*for_install = 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
*need_out = remaining;
|
||||
*free_out = free_b;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Built fresh per request so free space stays live (a 60+ GB download moves
|
||||
it a lot). g_fw is read-only after startup, so this is thread-safe. */
|
||||
/* The console's LAN address, so a page opened on the PS5 itself (127.0.0.1)
|
||||
@@ -1457,8 +1524,9 @@ build_downloads_json(void) {
|
||||
jbuf_appendf(&j, ",\"rc\":%d,\"last_curl_rc\":%d,\"last_http_code\":%ld",
|
||||
job->rc, job->last_curl_rc, job->last_http_code);
|
||||
if (job->rc == -3)
|
||||
jbuf_appendf(&j, ",\"need_bytes\":%lld,\"free_bytes\":%lld",
|
||||
job->need_bytes, job->free_bytes);
|
||||
jbuf_appendf(&j, ",\"need_bytes\":%lld,\"free_bytes\":%lld%s",
|
||||
job->need_bytes, job->free_bytes,
|
||||
job->space_install ? ",\"space_install\":true" : "");
|
||||
if (job->fail_step[0]) {
|
||||
jbuf_append(&j, ",\"fail_step\":");
|
||||
jbuf_append_str(&j, job->fail_step);
|
||||
@@ -2178,7 +2246,8 @@ admit_next(void) {
|
||||
dl_job_t *q = NULL;
|
||||
patchdl_manifest_t mf;
|
||||
char murl[768], dest[320], dir[288];
|
||||
int is_manifest, fd = -1, ok = 0, no_space = 0;
|
||||
int is_manifest, fd = -1, ok = 0, no_space = 0, space_inst = 0;
|
||||
char q_title[32];
|
||||
unsigned char *pre_done = NULL; /* pieces proven good by hash */
|
||||
char fstep[24] = "";
|
||||
int fcurl = 0;
|
||||
@@ -2201,6 +2270,7 @@ admit_next(void) {
|
||||
snprintf(murl, sizeof murl, "%s", q->manifest_url);
|
||||
snprintf(dest, sizeof dest, "%s", q->dest);
|
||||
snprintf(dir, sizeof dir, "%s", q->dir);
|
||||
snprintf(q_title, sizeof q_title, "%s", q->title_id);
|
||||
pthread_mutex_unlock(&g_pool.mtx);
|
||||
|
||||
/* ---- network + disk, NO pool lock ---- */
|
||||
@@ -2214,8 +2284,24 @@ admit_next(void) {
|
||||
total = mf.total;
|
||||
adopted = adopt_by_size(dir, dest, total);
|
||||
need = space_needed(dest, total);
|
||||
if (!space_ok(dir, need, &free_b)) no_space = 1;
|
||||
else fd = open(dest, O_RDWR | O_CREAT, 0666);
|
||||
/* The install needs about the package's size again on the
|
||||
game's drive. Check that now, before downloading something
|
||||
that can't be installed: on the same drive the download and
|
||||
the install share the free space. */
|
||||
{
|
||||
char gdir[256] = "";
|
||||
pthread_mutex_lock(&g_mutex);
|
||||
for (size_t i = 0; i < g_title_count; i++)
|
||||
if (!strcmp(g_titles[i].title_id, q_title)) {
|
||||
snprintf(gdir, sizeof gdir, "%s", g_titles[i].source_path);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mutex);
|
||||
if (!download_and_install_fit(dir, gdir, need, total,
|
||||
&need, &free_b, &space_inst))
|
||||
no_space = 1;
|
||||
}
|
||||
if (!no_space) fd = open(dest, O_RDWR | O_CREAT, 0666);
|
||||
if (fd >= 0) {
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) == 0) old_size = (long long)st.st_size;
|
||||
@@ -2289,6 +2375,7 @@ admit_next(void) {
|
||||
q->rc = no_space ? -3 : -1;
|
||||
q->need_bytes = need;
|
||||
q->free_bytes = free_b;
|
||||
q->space_install = space_inst;
|
||||
/* the manifest fetch failed: say where, so a report can tell a
|
||||
blocked host from a TLS or HTTP problem */
|
||||
snprintf(q->fail_step, sizeof q->fail_step, "%s", fstep);
|
||||
@@ -3055,7 +3142,7 @@ static enum MHD_Result
|
||||
do_install(struct MHD_Connection *conn, const char *title_id,
|
||||
patchdl_source_t src, const char *patch_url, const char *delta_url,
|
||||
const char *patch_title_id, const char *patch_storage_title_id,
|
||||
const char *content_id, const char *version, int enabled) {
|
||||
const char *content_id, const char *version, int enabled, int force) {
|
||||
char dest[320], msg[256], resp[720];
|
||||
int rc;
|
||||
install_stream_t *st = NULL;
|
||||
@@ -3088,6 +3175,32 @@ do_install(struct MHD_Connection *conn, const char *title_id,
|
||||
: "{\"ok\":false,\"reason\":\"download_incomplete\"}");
|
||||
}
|
||||
|
||||
/* The console writes the update next to the game and needs about the
|
||||
package's size free there; with less, its installer reads the header
|
||||
and quietly stops. Ask before starting (force = the user said go). */
|
||||
if (!force) {
|
||||
char gdir[256] = "";
|
||||
struct stat pst;
|
||||
pthread_mutex_lock(&g_mutex);
|
||||
for (size_t i = 0; i < g_title_count; i++)
|
||||
if (!strcmp(g_titles[i].title_id, title_id)) {
|
||||
snprintf(gdir, sizeof gdir, "%s", g_titles[i].source_path);
|
||||
break;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mutex);
|
||||
title_pkg_path(title_id, patch_url, dest, sizeof(dest));
|
||||
if (gdir[0] && stat(dest, &pst) == 0) {
|
||||
long long need = (long long)pst.st_size + INSTALL_SPACE_MARGIN;
|
||||
long long free_b = game_free_bytes(gdir);
|
||||
if (free_b >= 0 && free_b < need) {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"ok\":false,\"reason\":\"install_space\","
|
||||
"\"need_bytes\":%lld,\"free_bytes\":%lld}", need, free_b);
|
||||
return queue_json_owned(conn, MHD_HTTP_CONFLICT, strdup(resp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* GUARD (app layer): the version.xml target title id must match the game. */
|
||||
if (patch_title_id[0] && strncmp(patch_title_id, title_id, 9) != 0) {
|
||||
snprintf(resp, sizeof(resp),
|
||||
@@ -3447,10 +3560,12 @@ handle_title_action(struct MHD_Connection *conn, const char *url) {
|
||||
return queue_json(conn, MHD_HTTP_ACCEPTED,
|
||||
"{\"ok\":true,\"queued\":true,\"action\":\"check\"}");
|
||||
|
||||
if (!strcmp(action, "install"))
|
||||
if (!strcmp(action, "install")) {
|
||||
const char *f = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "force");
|
||||
return do_install(conn, title_id, src, patch_url, delta_url,
|
||||
patch_title_id, patch_storage_title_id,
|
||||
content_id, version, enabled);
|
||||
content_id, version, enabled, f && !strcmp(f, "1"));
|
||||
}
|
||||
|
||||
return queue_text(conn, MHD_HTTP_NOT_FOUND, "not found");
|
||||
}
|
||||
|
||||
+22
-7
@@ -340,7 +340,7 @@ async function openDiag(titleId) {
|
||||
diagRow(Boolean(mf && mf.ok), t("diag.manifest"), mfDetail),
|
||||
];
|
||||
if (job) rows.push(diagRow(job.state !== "error", t("diag.lastDownload"),
|
||||
job.state === "error" ? (Number(job.rc) === -3 ? noSpaceText(job.need_bytes, job.free_bytes)
|
||||
job.state === "error" ? (Number(job.rc) === -3 ? noSpaceText(job.need_bytes, job.free_bytes, job.space_install)
|
||||
: job.fail_step ? failStepText(job) : t("diag.failedPieces"))
|
||||
: tCode("diag.jobState.", job.state, job.state)));
|
||||
|
||||
@@ -922,7 +922,7 @@ function cardNotesHtml(game) {
|
||||
const notes = [];
|
||||
const job = state.downloads.find((d) => d.title_id === game.title_id);
|
||||
if (job && job.state === "error" && Number(job.rc) === -3)
|
||||
notes.push(`<div class="callout is-warn">${icon("alert")}<span>${escapeHtml(noSpaceText(job.need_bytes, job.free_bytes))}</span></div>`);
|
||||
notes.push(`<div class="callout is-warn">${icon("alert")}<span>${escapeHtml(noSpaceText(job.need_bytes, job.free_bytes, job.space_install))}</span></div>`);
|
||||
else if (job && job.state === "error")
|
||||
notes.push(`<div class="callout is-warn">${icon("alert")}<span>${escapeHtml(t("card.downloadFailed"))}${job.fail_step ? " " + escapeHtml(failStepText(job)) : ""}</span>
|
||||
<button type="button" class="link-button" data-diag="${escapeHtml(game.title_id)}">${t("card.details")}</button></div>`);
|
||||
@@ -1079,7 +1079,7 @@ function reconcileFromJobs(jobs) {
|
||||
// red Cancel stays available to delete the kept partial.
|
||||
const bytes = Number(j.bytes) || 0;
|
||||
if (Number(j.rc) === -3) {
|
||||
if (!g._spaceWarned) showToast(t("toast.nameMessage", { name: g.name, message: noSpaceText(j.need_bytes, j.free_bytes) }));
|
||||
if (!g._spaceWarned) showToast(t("toast.nameMessage", { name: g.name, message: noSpaceText(j.need_bytes, j.free_bytes, j.space_install) }));
|
||||
g._spaceWarned = true;
|
||||
} else if (g._wasActive) {
|
||||
const why = j.fail_step ? ` ${failStepText(j)}` : "";
|
||||
@@ -1547,7 +1547,7 @@ async function doDownload(game) {
|
||||
}
|
||||
}
|
||||
|
||||
async function doInstall(game) {
|
||||
async function doInstall(game, force = false) {
|
||||
const wasDownloaded = game.downloaded;
|
||||
game.installing = true;
|
||||
game.installStatus = "starting";
|
||||
@@ -1555,9 +1555,22 @@ async function doInstall(game) {
|
||||
game.downloaded = false; // the package is being consumed by the install
|
||||
renderGames();
|
||||
try {
|
||||
await postJson(API.action(game.title_id, "install"), {});
|
||||
await postJson(API.action(game.title_id, "install") + (force ? "?force=1" : ""), {});
|
||||
} catch (error) {
|
||||
game.downloaded = wasDownloaded; // refused: the package is untouched
|
||||
const b = (error && error.body) || {};
|
||||
if (b.reason === "install_space" && !force) {
|
||||
// Little room on the game's drive: the console would likely stop
|
||||
// right after reading the header. Let the user decide.
|
||||
game.installing = false;
|
||||
renderGames();
|
||||
if (window.confirm(t("install.lowSpace", {
|
||||
name: game.name, need: formatBytes(b.need_bytes), free: formatBytes(b.free_bytes) })))
|
||||
return doInstall(game, true);
|
||||
addLog("log.installBlocked", { id: game.title_id, why: t("reason.install_space") });
|
||||
renderLogs();
|
||||
return false;
|
||||
}
|
||||
if (error.body && error.body.reason === "install_running") {
|
||||
// Another tab or device started it: follow that install here.
|
||||
startInstallPolling();
|
||||
@@ -1681,8 +1694,10 @@ async function postJson(url, body) {
|
||||
|
||||
// Server refusal reasons are translated as reason.<code> in i18n.js.
|
||||
// The server keeps 2 GB free on /data on top of the package itself.
|
||||
function noSpaceText(need, free) {
|
||||
return t("reason.noSpace", { need: formatBytes(need), free: formatBytes(free) });
|
||||
// install: the download fit, but installing it afterwards wouldn't.
|
||||
function noSpaceText(need, free, install) {
|
||||
return t(install ? "reason.noSpaceInstall" : "reason.noSpace",
|
||||
{ need: formatBytes(need), free: formatBytes(free) });
|
||||
}
|
||||
|
||||
function reasonText(error) {
|
||||
|
||||
+30
@@ -103,6 +103,8 @@ const I18N = {
|
||||
"progress.queued": "Queued — waiting for a free slot",
|
||||
"progress.verifying": "Checking the package already on disk",
|
||||
"progress.retrying": "Connection interrupted, trying again…",
|
||||
"install.lowSpace": "{name}: the console probably needs about {need} free on the game's drive to install this update, but only {free} is free there. The install may stop right at the start. Install anyway?",
|
||||
"reason.install_space": "Not enough free space to install",
|
||||
"progress.eta": "ETA <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "Fetching manifest…",
|
||||
"install.phase.waiting": "Waiting for the console",
|
||||
@@ -324,6 +326,7 @@ const I18N = {
|
||||
"reason.fallback": "failed",
|
||||
"reason.notOrbisLink": "That isn't a link from Orbis Patches. Press “Copy all links!” there and paste what it copies.",
|
||||
"reason.noSpace": "Not enough space on the PS5: needs {need} plus 2 GB headroom, {free} free.",
|
||||
"reason.noSpaceInstall": "Not enough space to download and install this update: it needs about {need}, {free} is free. Free up space, or pick another download drive in Settings.",
|
||||
|
||||
// ---- older patch guide ----
|
||||
"guide.eyebrowOther": "Other version",
|
||||
@@ -453,6 +456,8 @@ I18N["de"] = {
|
||||
"progress.queued": "In Warteschlange — wartet auf einen freien Platz",
|
||||
"progress.verifying": "Vorhandenes Paket auf der Konsole wird geprüft",
|
||||
"progress.retrying": "Verbindung unterbrochen, neuer Versuch…",
|
||||
"install.lowSpace": "{name}: Zum Installieren braucht die Konsole auf dem Laufwerk des Spiels wahrscheinlich etwa {need} frei, dort sind aber nur {free} frei. Die Installation könnte gleich am Anfang abbrechen. Trotzdem installieren?",
|
||||
"reason.install_space": "Nicht genug Platz zum Installieren",
|
||||
"progress.eta": "Restzeit <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "Paketliste wird geladen…",
|
||||
"install.phase.waiting": "Warten auf die Konsole",
|
||||
@@ -653,6 +658,7 @@ I18N["de"] = {
|
||||
"reason.source_not_supported": "Downloads werden für diese Quelle nicht unterstützt.",
|
||||
"reason.fallback": "fehlgeschlagen",
|
||||
"reason.noSpace": "Nicht genug Speicher auf der PS5: benötigt {need} plus 2 GB Reserve, {free} frei.",
|
||||
"reason.noSpaceInstall": "Nicht genug Platz, um dieses Update herunterzuladen und zu installieren: Es braucht etwa {need}, frei sind {free}. Schaffe Platz oder wähle in den Einstellungen ein anderes Download-Laufwerk.",
|
||||
"guide.eyebrowOther": "Andere Version",
|
||||
"guide.eyebrowOld": "Patch für alte FW",
|
||||
"guide.title": "{version} herunterladen",
|
||||
@@ -776,6 +782,8 @@ I18N["fr"] = {
|
||||
"progress.queued": "En file d'attente — en attente d'un emplacement libre",
|
||||
"progress.verifying": "Vérification du paquet déjà présent sur la console",
|
||||
"progress.retrying": "Connexion interrompue, nouvel essai…",
|
||||
"install.lowSpace": "{name} : pour installer cette mise à jour, la console a probablement besoin d'environ {need} libres sur le disque du jeu, mais il n'y a que {free} de libre. L'installation risque de s'arrêter dès le début. Installer quand même ?",
|
||||
"reason.install_space": "Pas assez d'espace libre pour installer",
|
||||
"progress.eta": "Temps restant <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "Récupération de la liste des paquets…",
|
||||
"install.phase.waiting": "En attente de la console",
|
||||
@@ -976,6 +984,7 @@ I18N["fr"] = {
|
||||
"reason.source_not_supported": "Les téléchargements ne sont pas pris en charge pour cette source.",
|
||||
"reason.fallback": "échec",
|
||||
"reason.noSpace": "Pas assez d'espace sur la PS5 : il faut {need} plus 2 Go de marge, {free} libres.",
|
||||
"reason.noSpaceInstall": "Pas assez d'espace pour télécharger et installer cette mise à jour : il faut environ {need}, {free} sont libres. Libérez de l'espace ou choisissez un autre disque de téléchargement dans les paramètres.",
|
||||
"guide.eyebrowOther": "Autre version",
|
||||
"guide.eyebrowOld": "Patch pour ancien FW",
|
||||
"guide.title": "Télécharger {version}",
|
||||
@@ -1099,6 +1108,8 @@ I18N["es"] = {
|
||||
"progress.queued": "En cola — esperando un hueco libre",
|
||||
"progress.verifying": "Comprobando el paquete que ya está en la consola",
|
||||
"progress.retrying": "Conexión interrumpida, reintentando…",
|
||||
"install.lowSpace": "{name}: para instalar esta actualización, la consola probablemente necesita unos {need} libres en la unidad del juego, pero solo hay {free} libres. La instalación podría detenerse al principio. ¿Instalar de todos modos?",
|
||||
"reason.install_space": "No hay espacio suficiente para instalar",
|
||||
"progress.eta": "Tiempo restante <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "Obteniendo la lista de paquetes…",
|
||||
"install.phase.waiting": "Esperando a la consola",
|
||||
@@ -1299,6 +1310,7 @@ I18N["es"] = {
|
||||
"reason.source_not_supported": "No se admiten descargas para esta fuente.",
|
||||
"reason.fallback": "error",
|
||||
"reason.noSpace": "No hay suficiente espacio en la PS5: se necesitan {need} más 2 GB de margen, {free} libres.",
|
||||
"reason.noSpaceInstall": "No hay espacio suficiente para descargar e instalar esta actualización: necesita unos {need} y hay {free} libres. Libera espacio o elige otra unidad de descarga en Ajustes.",
|
||||
"guide.eyebrowOther": "Otra versión",
|
||||
"guide.eyebrowOld": "Parche para FW antiguo",
|
||||
"guide.title": "Descargar {version}",
|
||||
@@ -1422,6 +1434,8 @@ I18N["it"] = {
|
||||
"progress.queued": "In coda — in attesa di uno slot libero",
|
||||
"progress.verifying": "Verifica del pacchetto già presente sul disco",
|
||||
"progress.retrying": "Connessione interrotta, nuovo tentativo…",
|
||||
"install.lowSpace": "{name}: per installare questo aggiornamento la console probabilmente ha bisogno di circa {need} liberi sull'unità del gioco, ma ce ne sono solo {free}. L'installazione potrebbe fermarsi subito. Installare comunque?",
|
||||
"reason.install_space": "Spazio insufficiente per l'installazione",
|
||||
"progress.eta": "Fine tra <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "Recupero elenco pacchetti…",
|
||||
"install.phase.waiting": "In attesa della console",
|
||||
@@ -1622,6 +1636,7 @@ I18N["it"] = {
|
||||
"reason.source_not_supported": "I download non sono supportati per questa origine.",
|
||||
"reason.fallback": "non riuscito",
|
||||
"reason.noSpace": "Spazio insufficiente sulla PS5: servono {need} più 2 GB di margine, {free} liberi.",
|
||||
"reason.noSpaceInstall": "Spazio insufficiente per scaricare e installare questo aggiornamento: servono circa {need}, ne sono liberi {free}. Libera spazio o scegli un'altra unità di download nelle Impostazioni.",
|
||||
"guide.eyebrowOther": "Altra versione",
|
||||
"guide.eyebrowOld": "Patch per FW precedente",
|
||||
"guide.title": "Scarica {version}",
|
||||
@@ -1745,6 +1760,8 @@ I18N["pt-BR"] = {
|
||||
"progress.queued": "Na fila — aguardando uma vaga livre",
|
||||
"progress.verifying": "Verificando o pacote que já está no disco",
|
||||
"progress.retrying": "Conexão interrompida, tentando de novo…",
|
||||
"install.lowSpace": "{name}: para instalar esta atualização, o console provavelmente precisa de cerca de {need} livres na unidade do jogo, mas só há {free} livres. A instalação pode parar logo no início. Instalar mesmo assim?",
|
||||
"reason.install_space": "Espaço insuficiente para instalar",
|
||||
"progress.eta": "Faltam <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "Obtendo lista de pacotes…",
|
||||
"install.phase.waiting": "Aguardando o console",
|
||||
@@ -1945,6 +1962,7 @@ I18N["pt-BR"] = {
|
||||
"reason.source_not_supported": "Downloads não são suportados para esta origem.",
|
||||
"reason.fallback": "falhou",
|
||||
"reason.noSpace": "Espaço insuficiente no PS5: são necessários {need} mais 2 GB de folga, {free} livres.",
|
||||
"reason.noSpaceInstall": "Espaço insuficiente para baixar e instalar esta atualização: ela precisa de cerca de {need} e há {free} livres. Libere espaço ou escolha outra unidade de download em Configurações.",
|
||||
"guide.eyebrowOther": "Outra versão",
|
||||
"guide.eyebrowOld": "Patch de FW antigo",
|
||||
"guide.title": "Baixar {version}",
|
||||
@@ -2068,6 +2086,8 @@ I18N["ja"] = {
|
||||
"progress.queued": "待機中 — 空きスロットを待っています",
|
||||
"progress.verifying": "ディスク上の既存パッケージを確認しています",
|
||||
"progress.retrying": "接続が中断されました。再試行しています…",
|
||||
"install.lowSpace": "{name}: このアップデートのインストールには、ゲームのドライブに約 {need} の空き容量が必要と思われますが、空きは {free} しかありません。インストールが最初に止まる可能性があります。それでもインストールしますか?",
|
||||
"reason.install_space": "インストールに必要な空き容量が足りません",
|
||||
"progress.eta": "残り <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "パッケージリストを取得中…",
|
||||
"install.phase.waiting": "本体を待っています",
|
||||
@@ -2268,6 +2288,7 @@ I18N["ja"] = {
|
||||
"reason.source_not_supported": "このソースのダウンロードには対応していません。",
|
||||
"reason.fallback": "失敗",
|
||||
"reason.noSpace": "PS5 の空き容量が不足しています: {need} と予備の 2 GB が必要ですが、空きは {free} です。",
|
||||
"reason.noSpaceInstall": "このアップデートをダウンロードしてインストールするには空き容量が足りません。約 {need} 必要ですが、空きは {free} です。空き容量を増やすか、設定で別のダウンロード先ドライブを選んでください。",
|
||||
"guide.eyebrowOther": "他のバージョン",
|
||||
"guide.eyebrowOld": "旧 FW 用パッチ",
|
||||
"guide.title": "{version} をダウンロード",
|
||||
@@ -2391,6 +2412,8 @@ I18N["zh-Hans"] = {
|
||||
"progress.queued": "排队中 — 正在等待空闲位置",
|
||||
"progress.verifying": "正在检查磁盘上已有的安装包",
|
||||
"progress.retrying": "连接中断,正在重试…",
|
||||
"install.lowSpace": "{name}:安装此更新时,主机可能需要游戏所在驱动器上约 {need} 的可用空间,但那里只有 {free} 可用。安装可能会在一开始就停止。仍要安装吗?",
|
||||
"reason.install_space": "可用空间不足,无法安装",
|
||||
"progress.eta": "剩余 <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "正在获取包列表…",
|
||||
"install.phase.waiting": "正在等待主机",
|
||||
@@ -2591,6 +2614,7 @@ I18N["zh-Hans"] = {
|
||||
"reason.source_not_supported": "此来源不支持下载。",
|
||||
"reason.fallback": "失败",
|
||||
"reason.noSpace": "PS5 空间不足:需要 {need} 外加 2 GB 余量,当前可用 {free}。",
|
||||
"reason.noSpaceInstall": "空间不足,无法下载并安装此更新:大约需要 {need},当前可用 {free}。请释放空间,或在设置中选择其他下载驱动器。",
|
||||
"guide.eyebrowOther": "其他版本",
|
||||
"guide.eyebrowOld": "旧 FW 补丁",
|
||||
"guide.title": "下载 {version}",
|
||||
@@ -2714,6 +2738,8 @@ I18N["ko"] = {
|
||||
"progress.queued": "대기 중 — 빈 슬롯을 기다리는 중",
|
||||
"progress.verifying": "디스크에 이미 있는 패키지를 확인하는 중",
|
||||
"progress.retrying": "연결이 끊겼습니다. 다시 시도하는 중…",
|
||||
"install.lowSpace": "{name}: 이 업데이트를 설치하려면 게임이 있는 드라이브에 약 {need}의 여유 공간이 필요할 것으로 보이지만, 여유 공간은 {free}뿐입니다. 설치가 시작하자마자 멈출 수 있습니다. 그래도 설치하시겠습니까?",
|
||||
"reason.install_space": "설치할 여유 공간이 부족합니다",
|
||||
"progress.eta": "남은 시간 <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "패키지 목록을 가져오는 중…",
|
||||
"install.phase.waiting": "본체를 기다리는 중",
|
||||
@@ -2914,6 +2940,7 @@ I18N["ko"] = {
|
||||
"reason.source_not_supported": "이 소스는 다운로드가 지원되지 않습니다.",
|
||||
"reason.fallback": "실패",
|
||||
"reason.noSpace": "PS5의 공간이 부족합니다: {need}에 여유분 2 GB가 더 필요하지만, 여유 공간은 {free}입니다.",
|
||||
"reason.noSpaceInstall": "이 업데이트를 다운로드하고 설치할 공간이 부족합니다. 약 {need}이(가) 필요하지만 여유 공간은 {free}입니다. 공간을 확보하거나 설정에서 다른 다운로드 드라이브를 선택하세요.",
|
||||
"guide.eyebrowOther": "다른 버전",
|
||||
"guide.eyebrowOld": "이전 FW용 패치",
|
||||
"guide.title": "{version} 다운로드",
|
||||
@@ -3037,6 +3064,8 @@ I18N["ru"] = {
|
||||
"progress.queued": "В очереди — ожидание свободного слота",
|
||||
"progress.verifying": "Проверка пакета, который уже есть на диске",
|
||||
"progress.retrying": "Соединение прервано, повторная попытка…",
|
||||
"install.lowSpace": "{name}: для установки этого обновления консоли, вероятно, нужно около {need} свободного места на диске с игрой, а там свободно только {free}. Установка может остановиться в самом начале. Всё равно установить?",
|
||||
"reason.install_space": "Недостаточно места для установки",
|
||||
"progress.eta": "Осталось <b>{eta}</b>",
|
||||
"progress.fetchingManifest": "Получение списка пакетов…",
|
||||
"install.phase.waiting": "Ожидание консоли",
|
||||
@@ -3237,6 +3266,7 @@ I18N["ru"] = {
|
||||
"reason.source_not_supported": "Для этого источника загрузки не поддерживаются.",
|
||||
"reason.fallback": "ошибка",
|
||||
"reason.noSpace": "Недостаточно места на PS5: нужно {need} плюс 2 ГБ запаса, свободно {free}.",
|
||||
"reason.noSpaceInstall": "Недостаточно места, чтобы загрузить и установить это обновление: нужно около {need}, свободно {free}. Освободите место или выберите другой диск для загрузок в настройках.",
|
||||
"guide.eyebrowOther": "Другая версия",
|
||||
"guide.eyebrowOld": "Патч для старой FW",
|
||||
"guide.title": "Скачать {version}",
|
||||
|
||||
Reference in new issue
Block a user