mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-21 19:33:08 -05:00
Fixes from issues and Discord: a full drive's partial file, stopping an install early, 0x80B21104, VPN
- A single-file upload that fills the drive keeps <file>.ava-part so Retry resumes it, but nothing said so: the failed row now names it and offers Delete partial file (the next attempt starts fresh). - Stopping a stream install before the engine had its job dropped the row and left the install running unseen; Stop now waits for the job and stops it. - 0x80B21104 explains that the console will not take this package and links its FAQ entry, instead of the generic PlayGo text. - The Connection screen's "port not open" step suggests turning a VPN off (reported twice on Discord). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ub1mez9eaVtTF8pMEFXvA9
This commit is contained in:
1 parent
7fd748cb03
commit
5f93259bf5
31 files changed
+305
-5
No files matched your search
@@ -3747,6 +3747,12 @@ activity_showing_console: "يُعرض {name}",
|
||||
activity_show_this_console: "اعرض هذا الجهاز",
|
||||
game_open_page: "افتح صفحة اللعبة",
|
||||
game_already_on: "{name} لديه هذا بالفعل",
|
||||
queue_partial_kept: "يُحتفظ بما أُرسل على PS5 باسم {path}، لكي تُكمل إعادة المحاولة من حيث توقفت. يشغل مساحة: احذفه إن لم تعد المحاولة.",
|
||||
queue_partial_delete: "احذف الملف الجزئي",
|
||||
queue_partial_delete_title: "حذف الملف الجزئي؟",
|
||||
queue_partial_delete_body: "سيُحذف {path} من PS5، ثم يبدأ الرفع من البداية.",
|
||||
queue_partial_deleted: "حُذف الملف الجزئي. إعادة المحاولة تبدأ الرفع من جديد.",
|
||||
connection_vpn_hint: "إن كانت شبكة VPN تعمل، أوقفها (أو اسمح فيها بالوصول إلى الشبكة المحلية) ثم تحقق مجددًا.",
|
||||
};
|
||||
|
||||
export default ar;
|
||||
@@ -3747,6 +3747,12 @@ activity_showing_console: "{name} দেখানো হচ্ছে",
|
||||
activity_show_this_console: "এই কনসোল দেখান",
|
||||
game_open_page: "গেম পেজ খুলুন",
|
||||
game_already_on: "{name}-এ এটি আগে থেকেই আছে",
|
||||
queue_partial_kept: "যা পাঠানো হয়েছে তা PS5-এ {path} হিসেবে রাখা আছে, যাতে আবার চেষ্টা থামার জায়গা থেকে চালিয়ে যায়। এটি জায়গা নেয়: আবার চেষ্টা না করলে মুছে ফেলুন।",
|
||||
queue_partial_delete: "অসম্পূর্ণ ফাইল মুছুন",
|
||||
queue_partial_delete_title: "অসম্পূর্ণ ফাইল মুছবেন?",
|
||||
queue_partial_delete_body: "{path} PS5 থেকে মুছে ফেলা হবে। তারপর আপলোড শুরু থেকে হবে।",
|
||||
queue_partial_deleted: "অসম্পূর্ণ ফাইল মুছে ফেলা হয়েছে। আবার চেষ্টা শুরু থেকে আপলোড করবে।",
|
||||
connection_vpn_hint: "VPN চালু থাকলে বন্ধ করুন (বা এতে লোকাল নেটওয়ার্ক অ্যাক্সেস দিন), তারপর আবার যাচাই করুন।",
|
||||
};
|
||||
|
||||
export default bn;
|
||||
@@ -3755,6 +3755,12 @@ activity_showing_console: "{name} wird angezeigt",
|
||||
activity_show_this_console: "Diese Konsole anzeigen",
|
||||
game_open_page: "Spielseite öffnen",
|
||||
game_already_on: "{name} hat es bereits",
|
||||
queue_partial_kept: "Das bereits Gesendete bleibt auf der PS5 als {path}, damit „Erneut versuchen“ dort weitermacht. Es belegt Speicher: Lösche es, wenn du es nicht erneut versuchst.",
|
||||
queue_partial_delete: "Teildatei löschen",
|
||||
queue_partial_delete_title: "Teildatei löschen?",
|
||||
queue_partial_delete_body: "{path} wird von der PS5 gelöscht. Der Upload beginnt dann von vorn.",
|
||||
queue_partial_deleted: "Teildatei gelöscht. „Erneut versuchen“ beginnt den Upload von vorn.",
|
||||
connection_vpn_hint: "Wenn ein VPN aktiv ist, schalte es aus (oder erlaube darin den Zugriff aufs lokale Netzwerk) und prüfe erneut.",
|
||||
};
|
||||
|
||||
export default de;
|
||||
@@ -4229,6 +4229,12 @@ activity_showing_console: "Showing {name}",
|
||||
activity_show_this_console: "Show this console",
|
||||
game_open_page: "Open game page",
|
||||
game_already_on: "{name} has it already",
|
||||
queue_partial_kept: "What was sent is kept on the PS5 as {path}, so Retry continues where it stopped. It takes up space: delete it if you won't retry.",
|
||||
queue_partial_delete: "Delete partial file",
|
||||
queue_partial_delete_title: "Delete the partial file?",
|
||||
queue_partial_delete_body: "{path} will be deleted from the PS5. The upload then starts over from the beginning.",
|
||||
queue_partial_deleted: "Partial file deleted. Retry starts the upload over.",
|
||||
connection_vpn_hint: "If a VPN is on, turn it off (or allow local network access in it), then check again.",
|
||||
};
|
||||
|
||||
export default en;
|
||||
@@ -3694,6 +3694,12 @@ activity_showing_console: "Mostrando {name}",
|
||||
activity_show_this_console: "Mostrar esta consola",
|
||||
game_open_page: "Abrir la página del juego",
|
||||
game_already_on: "{name} ya lo tiene",
|
||||
queue_partial_kept: "Lo enviado se guarda en la PS5 como {path}, para que Reintentar continúe donde se detuvo. Ocupa espacio: bórralo si no vas a reintentar.",
|
||||
queue_partial_delete: "Borrar archivo parcial",
|
||||
queue_partial_delete_title: "¿Borrar el archivo parcial?",
|
||||
queue_partial_delete_body: "{path} se borrará de la PS5. La subida empezará de nuevo desde el principio.",
|
||||
queue_partial_deleted: "Archivo parcial borrado. Reintentar empieza la subida de nuevo.",
|
||||
connection_vpn_hint: "Si hay una VPN activa, desactívala (o permite en ella el acceso a la red local) y vuelve a comprobar.",
|
||||
};
|
||||
|
||||
export default es;
|
||||
@@ -4123,6 +4123,12 @@ activity_showing_console: "نمایش {name}",
|
||||
activity_show_this_console: "نمایش این کنسول",
|
||||
game_open_page: "باز کردن صفحه بازی",
|
||||
game_already_on: "{name} این را از قبل دارد",
|
||||
queue_partial_kept: "بخشِ ارسالشده روی PS5 با نام {path} نگه داشته میشود تا «تلاش دوباره» از همانجا ادامه دهد. فضا میگیرد؛ اگر دوباره تلاش نمیکنید حذفش کنید.",
|
||||
queue_partial_delete: "حذف فایل ناقص",
|
||||
queue_partial_delete_title: "فایل ناقص حذف شود؟",
|
||||
queue_partial_delete_body: "{path} از PS5 حذف میشود و آپلود از ابتدا شروع میشود.",
|
||||
queue_partial_deleted: "فایل ناقص حذف شد. «تلاش دوباره» آپلود را از ابتدا شروع میکند.",
|
||||
connection_vpn_hint: "اگر VPN روشن است، آن را خاموش کنید (یا در آن دسترسی به شبکه محلی را مجاز کنید) و دوباره بررسی کنید.",
|
||||
};
|
||||
|
||||
export default fa;
|
||||
@@ -3781,6 +3781,12 @@ activity_showing_console: "Affichage de {name}",
|
||||
activity_show_this_console: "Afficher cette console",
|
||||
game_open_page: "Ouvrir la page du jeu",
|
||||
game_already_on: "{name} l'a déjà",
|
||||
queue_partial_kept: "Ce qui a été envoyé est gardé sur la PS5 sous {path}, pour que Réessayer reprenne là où ça s'est arrêté. Il prend de la place : supprimez-le si vous ne réessayez pas.",
|
||||
queue_partial_delete: "Supprimer le fichier partiel",
|
||||
queue_partial_delete_title: "Supprimer le fichier partiel ?",
|
||||
queue_partial_delete_body: "{path} sera supprimé de la PS5. L'envoi reprendra alors depuis le début.",
|
||||
queue_partial_deleted: "Fichier partiel supprimé. Réessayer recommence l'envoi.",
|
||||
connection_vpn_hint: "Si un VPN est actif, désactivez-le (ou autorisez-y l'accès au réseau local), puis vérifiez à nouveau.",
|
||||
};
|
||||
|
||||
export default fr;
|
||||
@@ -3780,6 +3780,12 @@ activity_showing_console: "{name} दिखाया जा रहा है",
|
||||
activity_show_this_console: "यह कंसोल दिखाएँ",
|
||||
game_open_page: "गेम पेज खोलें",
|
||||
game_already_on: "{name} पर यह पहले से है",
|
||||
queue_partial_kept: "भेजा गया हिस्सा PS5 पर {path} के रूप में रखा गया है, ताकि फिर से कोशिश वहीं से जारी रखे। यह जगह घेरता है: अगर फिर से कोशिश नहीं करनी तो इसे हटा दें।",
|
||||
queue_partial_delete: "अधूरी फ़ाइल हटाएँ",
|
||||
queue_partial_delete_title: "अधूरी फ़ाइल हटाएँ?",
|
||||
queue_partial_delete_body: "{path} PS5 से हटा दी जाएगी। फिर अपलोड शुरू से होगा।",
|
||||
queue_partial_deleted: "अधूरी फ़ाइल हटा दी गई। फिर से कोशिश अपलोड शुरू से करेगा।",
|
||||
connection_vpn_hint: "अगर VPN चालू है तो उसे बंद करें (या उसमें लोकल नेटवर्क की अनुमति दें), फिर दोबारा जाँचें।",
|
||||
};
|
||||
|
||||
export default hi;
|
||||
@@ -4100,6 +4100,12 @@ activity_showing_console: "{name} látható",
|
||||
activity_show_this_console: "Ennek a konzolnak a megjelenítése",
|
||||
game_open_page: "Játékoldal megnyitása",
|
||||
game_already_on: "A(z) {name} már tartalmazza",
|
||||
queue_partial_kept: "Az elküldött rész a PS5-ön marad {path} néven, így az Újra onnan folytatja, ahol abbamaradt. Helyet foglal: töröld, ha nem próbálod újra.",
|
||||
queue_partial_delete: "Részleges fájl törlése",
|
||||
queue_partial_delete_title: "Törlöd a részleges fájlt?",
|
||||
queue_partial_delete_body: "A(z) {path} törlődik a PS5-ről. A feltöltés ezután elölről kezdődik.",
|
||||
queue_partial_deleted: "Részleges fájl törölve. Az Újra elölről kezdi a feltöltést.",
|
||||
connection_vpn_hint: "Ha VPN van bekapcsolva, kapcsold ki (vagy engedélyezd benne a helyi hálózat elérését), majd ellenőrizd újra.",
|
||||
};
|
||||
|
||||
export default hu;
|
||||
@@ -3756,6 +3756,12 @@ activity_showing_console: "Menampilkan {name}",
|
||||
activity_show_this_console: "Tampilkan konsol ini",
|
||||
game_open_page: "Buka halaman game",
|
||||
game_already_on: "{name} sudah memilikinya",
|
||||
queue_partial_kept: "Yang sudah terkirim disimpan di PS5 sebagai {path}, agar Coba lagi melanjutkan dari titik berhenti. File ini memakan ruang: hapus jika tidak akan mencoba lagi.",
|
||||
queue_partial_delete: "Hapus file parsial",
|
||||
queue_partial_delete_title: "Hapus file parsial?",
|
||||
queue_partial_delete_body: "{path} akan dihapus dari PS5. Unggahan lalu dimulai dari awal.",
|
||||
queue_partial_deleted: "File parsial dihapus. Coba lagi memulai unggahan dari awal.",
|
||||
connection_vpn_hint: "Jika VPN aktif, matikan (atau izinkan akses jaringan lokal di dalamnya), lalu periksa lagi.",
|
||||
};
|
||||
|
||||
export default id;
|
||||
@@ -3694,6 +3694,12 @@ activity_showing_console: "Mostra {name}",
|
||||
activity_show_this_console: "Mostra questa console",
|
||||
game_open_page: "Apri la pagina del gioco",
|
||||
game_already_on: "{name} ce l'ha già",
|
||||
queue_partial_kept: "Quanto inviato resta sulla PS5 come {path}, così Riprova continua da dove si era fermato. Occupa spazio: eliminalo se non riproverai.",
|
||||
queue_partial_delete: "Elimina file parziale",
|
||||
queue_partial_delete_title: "Eliminare il file parziale?",
|
||||
queue_partial_delete_body: "{path} verrà eliminato dalla PS5. Il caricamento ripartirà dall'inizio.",
|
||||
queue_partial_deleted: "File parziale eliminato. Riprova ricomincia il caricamento.",
|
||||
connection_vpn_hint: "Se è attiva una VPN, disattivala (o consenti in essa l'accesso alla rete locale), poi controlla di nuovo.",
|
||||
};
|
||||
|
||||
export default it;
|
||||
@@ -3785,6 +3785,12 @@ activity_showing_console: "{name} を表示中",
|
||||
activity_show_this_console: "この本体を表示",
|
||||
game_open_page: "ゲームページを開く",
|
||||
game_already_on: "{name} にはすでにあります",
|
||||
queue_partial_kept: "送信済みの部分は PS5 上に {path} として残り、再試行で続きから送れます。容量を使うので、再試行しない場合は削除してください。",
|
||||
queue_partial_delete: "途中のファイルを削除",
|
||||
queue_partial_delete_title: "途中のファイルを削除しますか?",
|
||||
queue_partial_delete_body: "{path} を PS5 から削除します。アップロードは最初からやり直しになります。",
|
||||
queue_partial_deleted: "途中のファイルを削除しました。再試行すると最初からアップロードします。",
|
||||
connection_vpn_hint: "VPN がオンならオフにして (または VPN でローカルネットワークへのアクセスを許可して)、もう一度確認してください。",
|
||||
};
|
||||
|
||||
export default ja;
|
||||
@@ -3782,6 +3782,12 @@ activity_showing_console: "{name} 표시 중",
|
||||
activity_show_this_console: "이 콘솔 표시",
|
||||
game_open_page: "게임 페이지 열기",
|
||||
game_already_on: "{name}에 이미 있습니다",
|
||||
queue_partial_kept: "보낸 부분은 PS5에 {path}(으)로 남아 있어 다시 시도하면 멈춘 곳부터 이어집니다. 공간을 차지하니 다시 시도하지 않을 거라면 삭제하세요.",
|
||||
queue_partial_delete: "부분 파일 삭제",
|
||||
queue_partial_delete_title: "부분 파일을 삭제할까요?",
|
||||
queue_partial_delete_body: "{path}이(가) PS5에서 삭제됩니다. 업로드는 처음부터 다시 시작됩니다.",
|
||||
queue_partial_deleted: "부분 파일을 삭제했습니다. 다시 시도하면 처음부터 업로드합니다.",
|
||||
connection_vpn_hint: "VPN이 켜져 있다면 끄거나(또는 VPN에서 로컬 네트워크 접근을 허용하고) 다시 확인하세요.",
|
||||
};
|
||||
|
||||
export default ko;
|
||||
@@ -4033,6 +4033,12 @@ activity_showing_console: "Wyświetlono: {name}",
|
||||
activity_show_this_console: "Pokaż tę konsolę",
|
||||
game_open_page: "Otwórz stronę gry",
|
||||
game_already_on: "{name} już to ma",
|
||||
queue_partial_kept: "Wysłana część zostaje na PS5 jako {path}, aby Ponów kontynuowało od miejsca przerwania. Zajmuje miejsce: usuń ją, jeśli nie ponowisz.",
|
||||
queue_partial_delete: "Usuń częściowy plik",
|
||||
queue_partial_delete_title: "Usunąć częściowy plik?",
|
||||
queue_partial_delete_body: "{path} zostanie usunięty z PS5. Przesyłanie zacznie się od początku.",
|
||||
queue_partial_deleted: "Częściowy plik usunięty. Ponów zaczyna przesyłanie od nowa.",
|
||||
connection_vpn_hint: "Jeśli VPN jest włączony, wyłącz go (lub zezwól w nim na dostęp do sieci lokalnej) i sprawdź ponownie.",
|
||||
};
|
||||
|
||||
export default pl;
|
||||
@@ -3779,6 +3779,12 @@ activity_showing_console: "Mostrando {name}",
|
||||
activity_show_this_console: "Mostrar este console",
|
||||
game_open_page: "Abrir a página do jogo",
|
||||
game_already_on: "O {name} já tem",
|
||||
queue_partial_kept: "O que foi enviado fica no PS5 como {path}, para que Tentar novamente continue de onde parou. Ele ocupa espaço: apague-o se não for tentar de novo.",
|
||||
queue_partial_delete: "Apagar arquivo parcial",
|
||||
queue_partial_delete_title: "Apagar o arquivo parcial?",
|
||||
queue_partial_delete_body: "{path} será apagado do PS5. O envio recomeça do início.",
|
||||
queue_partial_deleted: "Arquivo parcial apagado. Tentar novamente recomeça o envio.",
|
||||
connection_vpn_hint: "Se houver uma VPN ativa, desligue-a (ou permita nela o acesso à rede local) e verifique de novo.",
|
||||
};
|
||||
|
||||
export default pt_BR;
|
||||
@@ -3694,6 +3694,12 @@ activity_showing_console: "Показано: {name}",
|
||||
activity_show_this_console: "Показать эту консоль",
|
||||
game_open_page: "Открыть страницу игры",
|
||||
game_already_on: "На {name} это уже есть",
|
||||
queue_partial_kept: "Отправленное сохранено на PS5 как {path}, чтобы «Повторить» продолжило с места остановки. Оно занимает место: удалите его, если не будете повторять.",
|
||||
queue_partial_delete: "Удалить частичный файл",
|
||||
queue_partial_delete_title: "Удалить частичный файл?",
|
||||
queue_partial_delete_body: "{path} будет удалён с PS5. Загрузка начнётся заново.",
|
||||
queue_partial_deleted: "Частичный файл удалён. «Повторить» начнёт загрузку заново.",
|
||||
connection_vpn_hint: "Если включён VPN, выключите его (или разрешите в нём доступ к локальной сети) и проверьте снова.",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
@@ -3771,6 +3771,12 @@ activity_showing_console: "กำลังแสดง {name}",
|
||||
activity_show_this_console: "แสดงคอนโซลนี้",
|
||||
game_open_page: "เปิดหน้าเกม",
|
||||
game_already_on: "{name} มีอยู่แล้ว",
|
||||
queue_partial_kept: "ส่วนที่ส่งไปแล้วถูกเก็บไว้บน PS5 เป็น {path} เพื่อให้ลองอีกครั้งทำต่อจากจุดที่หยุด ไฟล์นี้ใช้พื้นที่ ลบได้หากจะไม่ลองอีก",
|
||||
queue_partial_delete: "ลบไฟล์ที่ยังไม่ครบ",
|
||||
queue_partial_delete_title: "ลบไฟล์ที่ยังไม่ครบหรือไม่?",
|
||||
queue_partial_delete_body: "{path} จะถูกลบจาก PS5 แล้วการอัปโหลดจะเริ่มใหม่ตั้งแต่ต้น",
|
||||
queue_partial_deleted: "ลบไฟล์ที่ยังไม่ครบแล้ว ลองอีกครั้งจะเริ่มอัปโหลดใหม่",
|
||||
connection_vpn_hint: "หากเปิด VPN อยู่ ให้ปิด (หรืออนุญาตการเข้าถึงเครือข่ายภายในใน VPN) แล้วตรวจอีกครั้ง",
|
||||
};
|
||||
|
||||
export default th;
|
||||
@@ -3754,6 +3754,12 @@ activity_showing_console: "{name} gösteriliyor",
|
||||
activity_show_this_console: "Bu konsolu göster",
|
||||
game_open_page: "Oyun sayfasını aç",
|
||||
game_already_on: "{name} üzerinde zaten var",
|
||||
queue_partial_kept: "Gönderilen kısım PS5'te {path} olarak tutulur, böylece Yeniden dene kaldığı yerden devam eder. Yer kaplar: yeniden denemeyeceksen sil.",
|
||||
queue_partial_delete: "Kısmi dosyayı sil",
|
||||
queue_partial_delete_title: "Kısmi dosya silinsin mi?",
|
||||
queue_partial_delete_body: "{path} PS5'ten silinecek. Yükleme baştan başlar.",
|
||||
queue_partial_deleted: "Kısmi dosya silindi. Yeniden dene yüklemeyi baştan başlatır.",
|
||||
connection_vpn_hint: "Bir VPN açıksa kapat (ya da içinde yerel ağ erişimine izin ver), sonra yeniden kontrol et.",
|
||||
};
|
||||
|
||||
export default tr;
|
||||
@@ -3749,6 +3749,12 @@ activity_showing_console: "Đang hiển thị {name}",
|
||||
activity_show_this_console: "Hiển thị máy này",
|
||||
game_open_page: "Mở trang trò chơi",
|
||||
game_already_on: "{name} đã có rồi",
|
||||
queue_partial_kept: "Phần đã gửi được giữ trên PS5 dưới tên {path}, để Thử lại tiếp tục từ chỗ dừng. Nó chiếm dung lượng: hãy xóa nếu bạn không thử lại.",
|
||||
queue_partial_delete: "Xóa tệp dở dang",
|
||||
queue_partial_delete_title: "Xóa tệp dở dang?",
|
||||
queue_partial_delete_body: "{path} sẽ bị xóa khỏi PS5. Việc tải lên sẽ bắt đầu lại từ đầu.",
|
||||
queue_partial_deleted: "Đã xóa tệp dở dang. Thử lại sẽ tải lên lại từ đầu.",
|
||||
connection_vpn_hint: "Nếu đang bật VPN, hãy tắt nó (hoặc cho phép truy cập mạng cục bộ trong VPN), rồi kiểm tra lại.",
|
||||
};
|
||||
|
||||
export default vi;
|
||||
@@ -3700,6 +3700,12 @@ activity_showing_console: "正在显示 {name}",
|
||||
activity_show_this_console: "显示此主机",
|
||||
game_open_page: "打开游戏页面",
|
||||
game_already_on: "{name} 上已经有了",
|
||||
queue_partial_kept: "已发送的部分以 {path} 保留在 PS5 上,重试时会从中断处继续。它会占用空间:如果不打算重试,请删除它。",
|
||||
queue_partial_delete: "删除未完成的文件",
|
||||
queue_partial_delete_title: "删除未完成的文件?",
|
||||
queue_partial_delete_body: "将从 PS5 删除 {path}。之后上传会从头开始。",
|
||||
queue_partial_deleted: "已删除未完成的文件。重试会从头开始上传。",
|
||||
connection_vpn_hint: "如果开着 VPN,请关闭它(或在 VPN 中允许访问本地网络),然后再检查一次。",
|
||||
};
|
||||
|
||||
export default zh_CN;
|
||||
@@ -3694,6 +3694,12 @@ activity_showing_console: "正在顯示 {name}",
|
||||
activity_show_this_console: "顯示此主機",
|
||||
game_open_page: "開啟遊戲頁面",
|
||||
game_already_on: "{name} 上已經有了",
|
||||
queue_partial_kept: "已傳送的部分以 {path} 保留在 PS5 上,重試時會從中斷處繼續。它會佔用空間:如果不打算重試,請刪除它。",
|
||||
queue_partial_delete: "刪除未完成的檔案",
|
||||
queue_partial_delete_title: "刪除未完成的檔案?",
|
||||
queue_partial_delete_body: "將從 PS5 刪除 {path}。之後上傳會從頭開始。",
|
||||
queue_partial_deleted: "已刪除未完成的檔案。重試會從頭開始上傳。",
|
||||
connection_vpn_hint: "如果開著 VPN,請關閉它(或在 VPN 中允許存取本地網路),然後再檢查一次。",
|
||||
};
|
||||
|
||||
export default zh_TW;
|
||||
@@ -349,3 +349,9 @@ describe("humanizePs5Error", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("0x80B21104", () => {
|
||||
it("says the console would not take this package, not a generic PlayGo error", () => {
|
||||
expect(humanizePs5Error("The PS5 declined the install. (0x80b21104)")).toContain("would not accept this package");
|
||||
});
|
||||
});
|
||||
@@ -420,6 +420,11 @@ export function humanizePs5Error(
|
||||
if (/0x80B22101/i.test(raw)) {
|
||||
return te("err_install_2101");
|
||||
}
|
||||
// ─── 0x80B2_1104 — the installer would not take this package ──────
|
||||
// Before the wildcard: it is the package, not PlayGo (FAQ "0x80B21104").
|
||||
if (/0x80B2_?1104/i.test(raw)) {
|
||||
return te("pkg.sony_80b21104");
|
||||
}
|
||||
if (/\b0x80B2[0-9A-Fa-f]{4}\b/i.test(raw)) {
|
||||
// Other 0x80B2_xxxx — PlayGo errors, not pkg-format errors.
|
||||
// Don't blame the file.
|
||||
|
||||
@@ -24,6 +24,8 @@ describe("installErrorDoc", () => {
|
||||
["missing base entitlement", undefined, DOC_ANCHORS.entitlement],
|
||||
["shows View product instead of Play", undefined, DOC_ANCHORS.viewProduct],
|
||||
["This PS4 game isn't playable on PS5", undefined, DOC_ANCHORS.ps4OnPs5],
|
||||
["The PS5 declined the install. (0x80b21104)", undefined, DOC_ANCHORS.declined1104],
|
||||
["x", 0x80b21104, DOC_ANCHORS.declined1104],
|
||||
];
|
||||
for (const [raw, code, query] of cases) {
|
||||
expect(installErrorDoc(raw, code)?.query, `${raw} / ${code}`).toBe(query);
|
||||
|
||||
@@ -25,9 +25,11 @@ export const DOC_ANCHORS = {
|
||||
ce: "CE-108255-1",
|
||||
wontLaunch: "won't launch",
|
||||
matrix: "support matrix",
|
||||
declined1104: "0x80B21104",
|
||||
} as const;
|
||||
|
||||
const RULES: Array<[RegExp, string]> = [
|
||||
[/0x80b21104/i, DOC_ANCHORS.declined1104],
|
||||
[/0x80b2116f|0x80b2150f|staged route|refused this package from its own storage/i, DOC_ANCHORS.stagedRefused],
|
||||
[/0x80431084|proxy server|proxy setting/i, DOC_ANCHORS.proxy],
|
||||
[
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { partialUploadPath } from "./partialUpload";
|
||||
|
||||
const item = (over: Record<string, unknown>) =>
|
||||
({
|
||||
status: "failed",
|
||||
sourceKind: "image",
|
||||
resolvedDest: "/data/homebrew/PPSA03671 Wolverine.ffpfsc",
|
||||
errorReason: "ava1_no_space",
|
||||
...over,
|
||||
}) as Parameters<typeof partialUploadPath>[0];
|
||||
|
||||
describe("partialUploadPath", () => {
|
||||
it("names the partial file a single-file upload left when the drive filled", () => {
|
||||
expect(partialUploadPath(item({}))).toBe("/data/homebrew/PPSA03671 Wolverine.ffpfsc.ava-part");
|
||||
expect(partialUploadPath(item({ sourceKind: "file", errorReason: "preflight_insufficient_space" }))).toBe(
|
||||
"/data/homebrew/PPSA03671 Wolverine.ffpfsc.ava-part",
|
||||
);
|
||||
});
|
||||
it("is null for other failures, folders and archives", () => {
|
||||
expect(partialUploadPath(item({ errorReason: "ava1_connect" }))).toBeNull();
|
||||
expect(partialUploadPath(item({ sourceKind: "folder" }))).toBeNull();
|
||||
expect(partialUploadPath(item({ sourceKind: "archive" }))).toBeNull();
|
||||
expect(partialUploadPath(item({ status: "done" }))).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// A single-file upload writes `<destination>.ava-part` on the console and renames it when it
|
||||
// is complete. When the drive fills up the partial file is kept so Retry resumes it, but it
|
||||
// holds its whole size: a user who gives up (or wants to free space another way) has to know
|
||||
// it is there to delete it.
|
||||
|
||||
import type { QueueItem } from "../state/uploadQueue";
|
||||
|
||||
const NO_SPACE = new Set(["ava1_no_space", "preflight_insufficient_space"]);
|
||||
|
||||
/** The partial file a failed single-file upload left on the console, or null. */
|
||||
export function partialUploadPath(
|
||||
it: Pick<QueueItem, "status" | "sourceKind" | "resolvedDest" | "errorReason">,
|
||||
): string | null {
|
||||
if (it.status !== "failed" || !it.errorReason || !NO_SPACE.has(it.errorReason)) return null;
|
||||
if (it.sourceKind !== "file" && it.sourceKind !== "image") return null;
|
||||
return it.resolvedDest ? `${it.resolvedDest}.ava-part` : null;
|
||||
}
|
||||
@@ -507,7 +507,9 @@ export default function ConnectionScreen() {
|
||||
)
|
||||
: looksLikeMacLocalNetworkBlock(probe.error, ua)
|
||||
? `${detail}. ${tr("connection_mac_local_network_hint", undefined, "macOS may be blocking this app from your local network. Open System Settings → Privacy & Security → Local Network, allow PS5Upload, then check again.")}`
|
||||
: detail,
|
||||
: // A VPN on this computer sends the console's address out through the tunnel: the
|
||||
// likeliest cause when everything else is right (reported twice on Discord).
|
||||
`${detail}. ${tr("connection_vpn_hint", undefined, "If a VPN is on, turn it off (or allow local network access in it), then check again.")}`,
|
||||
target,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import { BottleneckLine, JobLiveNotes, UnsettledLine } from "./Bottleneck";
|
||||
import { WhySlowPanel } from "./WhySlow";
|
||||
import { RarPasswordPrompt } from "./RarPasswordPrompt";
|
||||
import { rarPasswordProblem } from "../../lib/rarPassword";
|
||||
import { partialUploadPath } from "../../lib/partialUpload";
|
||||
import { isStreamUnreachableError } from "../../lib/networkFix";
|
||||
import { NetworkFixActions } from "../InstallPackage/NetworkFixActions";
|
||||
|
||||
@@ -1111,6 +1112,7 @@ export function QueueRow({
|
||||
detail={item.errorDetail}
|
||||
/>
|
||||
)}
|
||||
{partialUploadPath(item) && <PartialFileNote id={item.id} path={partialUploadPath(item)!} />}
|
||||
{item.status === "failed" && isStreamUnreachableError(item.error) && (
|
||||
<NetworkFixActions diag={null} />
|
||||
)}
|
||||
@@ -1396,6 +1398,67 @@ function FailedRowErrorCard({
|
||||
);
|
||||
}
|
||||
|
||||
/** A single-file upload that filled the drive keeps what it sent as `<file>.ava-part`, so
|
||||
* Retry continues it; that file holds the space, so it is named here with a way to delete it. */
|
||||
function PartialFileNote({ id, path }: { id: string; path: string }) {
|
||||
const tr = useTr();
|
||||
const { confirm, dialog } = useConfirm();
|
||||
const [state, setState] = useState<"idle" | "busy" | "gone">("idle");
|
||||
const [failed, setFailed] = useState<string | null>(null);
|
||||
if (state === "gone") {
|
||||
return (
|
||||
<p className="mt-2 text-xs text-[var(--color-good)]">
|
||||
{tr("queue_partial_deleted", undefined, "Partial file deleted. Retry starts the upload over.")}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const remove = async () => {
|
||||
const ok = await confirm({
|
||||
title: tr("queue_partial_delete_title", undefined, "Delete the partial file?"),
|
||||
message: tr(
|
||||
"queue_partial_delete_body",
|
||||
{ path },
|
||||
"{path} will be deleted from the PS5. The upload then starts over from the beginning.",
|
||||
),
|
||||
confirmLabel: tr("queue_partial_delete", undefined, "Delete partial file"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
setState("busy");
|
||||
setFailed(null);
|
||||
try {
|
||||
await useUploadQueueStore.getState().discardPartial(id);
|
||||
setState("gone");
|
||||
} catch (e) {
|
||||
setState("idle");
|
||||
setFailed(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="mt-2 text-xs text-[var(--color-muted)]" data-testid="queue-partial-file">
|
||||
{dialog}
|
||||
<p>
|
||||
{tr(
|
||||
"queue_partial_kept",
|
||||
{ path },
|
||||
"What was sent is kept on the PS5 as {path}, so Retry continues where it stopped. It takes up space: delete it if you won't retry.",
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
className="mt-1.5"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
leftIcon={<Trash2 size={12} />}
|
||||
loading={state === "busy"}
|
||||
onClick={() => void remove()}
|
||||
>
|
||||
{tr("queue_partial_delete", undefined, "Delete partial file")}
|
||||
</Button>
|
||||
{failed && <p className="mt-1 text-[var(--color-bad)]">{failed}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusIcon({ status }: { status: QueueItemStatus }) {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
|
||||
@@ -1300,6 +1300,25 @@ describe("install items", () => {
|
||||
await waitFor(() => !useUploadQueueStore.getState().items.some((i) => i.id === q.id));
|
||||
});
|
||||
|
||||
it("an install stopped before the engine has its job is stopped once the job comes", async () => {
|
||||
const api = await import("../api/ps5");
|
||||
vi.mocked(api.pkgInstallStop).mockClear();
|
||||
const q = useUploadQueueStore.getState().enqueueInstall({
|
||||
host, request: { via: "stream", source: "/early.pkg" }, displayName: "E",
|
||||
});
|
||||
await waitFor(() => calls.length === 1);
|
||||
useUploadQueueStore.getState().cancelItem(q.id);
|
||||
expect(vi.mocked(api.pkgInstallStop)).not.toHaveBeenCalled();
|
||||
// The engine answers with its job a moment later: that job is the one stopped.
|
||||
useUploadQueueStore.setState((st) => ({
|
||||
items: st.items.map((i) => (i.id === q.id ? { ...i, installJobId: "job-late" } : i)),
|
||||
}));
|
||||
await waitFor(() => vi.mocked(api.pkgInstallStop).mock.calls.length > 0);
|
||||
expect(vi.mocked(api.pkgInstallStop)).toHaveBeenCalledWith("job-late");
|
||||
gate.get("/early.pkg")!({ ok: false, message: "stopped" });
|
||||
await waitFor(() => !useUploadQueueStore.getState().items.some((i) => i.id === q.id));
|
||||
});
|
||||
|
||||
it("an install leaves uploads waiting for Start alone (#410)", async () => {
|
||||
const s = useUploadQueueStore.getState();
|
||||
s.add({
|
||||
|
||||
@@ -63,6 +63,7 @@ import { withConsolePrefix } from "./roster";
|
||||
import { hostOf, mgmtAddr } from "../lib/addr";
|
||||
import { log } from "./logs";
|
||||
import { isRemotePath } from "../lib/remotePath";
|
||||
import { partialUploadPath } from "../lib/partialUpload";
|
||||
import { materializeRemote, releaseCopy } from "../lib/materialize";
|
||||
|
||||
/** Local copies of server archives, by queue item: made when the item's turn comes (not at
|
||||
@@ -423,6 +424,9 @@ interface QueueState {
|
||||
* the person just typed. The password lives on the in-memory item only (the save redacts
|
||||
* it) and is never logged. False when the password is empty or the row is not a failed one. */
|
||||
retryWithPassword: (id: string, password: string) => boolean;
|
||||
/** Deletes the partial file a single-file upload left when the drive filled up, and gives
|
||||
* the row a new job id so its next attempt starts fresh. Throws with the console's reason. */
|
||||
discardPartial: (id: string) => Promise<void>;
|
||||
/** Re-drive one console's uploads that FAILED on a recoverable
|
||||
* (connection-class) error, then restart that console's drain loop.
|
||||
* This is the "slept past the in-loop recovery budget" case: a standby
|
||||
@@ -2077,13 +2081,23 @@ export const useUploadQueueStore = create<QueueState>((set, get) => {
|
||||
if (!item) return;
|
||||
// A running install: ask the engine to stop it (it stops serving the package; the PS5's
|
||||
// download then fails, and a download already cancelled on the PS5 is let go at once
|
||||
// instead of after the 5-minute stall bound). The row goes once the job has ended. Before
|
||||
// the engine has a job there is nothing on the wire yet: fall through to the plain cancel.
|
||||
if (item.sourceKind === "install" && item.status === "running" && item.installJobId) {
|
||||
// instead of after the 5-minute stall bound). The row goes once the job has ended.
|
||||
if (item.sourceKind === "install" && item.status === "running") {
|
||||
if (item.stopping) return;
|
||||
set((s) => ({ items: patchItem(s.items, id, { stopping: true }) }));
|
||||
void pkgInstallStop(item.installJobId).catch(() => undefined);
|
||||
void (async () => {
|
||||
// Stopped while the engine was still starting the install: its job id comes in a
|
||||
// moment, and only that stops it. Dropping the row alone left the install running
|
||||
// on the engine with nothing showing it.
|
||||
let job = item.installJobId ?? null;
|
||||
for (let i = 0; !job && i < 30; i++) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
const now = get().items.find((it) => it.id === id);
|
||||
if (!now || now.status !== "running") break;
|
||||
job = now.installJobId ?? null;
|
||||
}
|
||||
if (job) await pkgInstallStop(job).catch(() => undefined);
|
||||
else get().stopHost(hostOf(item.addr));
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const now = get().items.find((it) => it.id === id);
|
||||
if (!now || now.status !== "running") break;
|
||||
@@ -2092,6 +2106,9 @@ export const useUploadQueueStore = create<QueueState>((set, get) => {
|
||||
settle(id, { ok: false, message: "Stopped." });
|
||||
set((s) => ({ items: removeItem(s.items, id) }));
|
||||
scheduleSave();
|
||||
// Stopping the loop held the console's other waiting items: let them go on.
|
||||
const h = hostOf(item.addr);
|
||||
if (!job && nextPendingForHost(get().items, h)) void get().startHost(h);
|
||||
})();
|
||||
return;
|
||||
}
|
||||
@@ -2236,6 +2253,19 @@ export const useUploadQueueStore = create<QueueState>((set, get) => {
|
||||
return true;
|
||||
},
|
||||
|
||||
async discardPartial(id) {
|
||||
const item = get().items.find((candidate) => candidate.id === id);
|
||||
const path = item ? partialUploadPath(item) : null;
|
||||
if (!item || !path) return;
|
||||
await fsDelete(mgmtAddr(hostOf(item.addr)), path);
|
||||
set((s) => ({
|
||||
items: s.items.map((candidate) =>
|
||||
candidate.id === id ? { ...candidate, txIdHex: generateTxIdHex() } : candidate,
|
||||
),
|
||||
}));
|
||||
scheduleSave();
|
||||
},
|
||||
|
||||
async resumeFailedRecoverable(host) {
|
||||
if (!useUploadSettingsStore.getState().autoResume) return 0;
|
||||
const h = hostOf(host);
|
||||
@@ -2447,6 +2477,7 @@ export const useUploadQueueStore = create<QueueState>((set, get) => {
|
||||
retryFailed: () => undefined,
|
||||
retryItem: () => false,
|
||||
retryWithPassword: () => false,
|
||||
discardPartial: () => Promise.resolve(),
|
||||
resumeFailedRecoverable: () => Promise.resolve(0),
|
||||
setContinueOnFailure: () => undefined,
|
||||
start: () => Promise.resolve(),
|
||||
|
||||
Reference in new issue
Block a user