12 Commits
Author SHA1 Message Date
Manos 0be4325bc5 v5.0.0 - Complete Management Suite: Hardware Monitor, Screenshots, Launch Game, Save Manager, Game Details, stability overhaul 2026-04-19 12:38:14 +03:00
Manos 343a5a531e Update README for v4.2.2 - Stability & Mount Fix Release 2026-04-14 21:03:51 +03:00
Manos fe741b8e9f v4.2.1 - Bugfix: UI scrolling, file counting, frozen progress display 2026-02-08 14:40:54 +02:00
Manos 0123a55f52 Update Features section: fix buffer sizes, update parallel connections, add missing features (retry, duplicate dialog, storage display, per-file mutex, game mounting in server), remove outdated tags 2026-02-07 04:06:13 +02:00
Manos eb6003c8d4 Update README and CHANGELOG with client improvements and bug fixes (speed display, ETA smoothing, Shell IP fix, duplicate dialog) 2026-02-07 04:04:08 +02:00
Manos 5022381eeb v4.2.0 - Game Mounter Integration: Mount Games button, multi-path scanning, duplicate detection, auto-cleanup, PS5 notifications, bug fixes (IOVEC_ENTRY NULL crash, libSceAppInstUtil crash, system() hangs) 2026-02-07 04:00:09 +02:00
Manos 8cdd392576 Update Astrobot test results - 53 minutes for 156K files 2026-02-05 02:29:53 +02:00
Manos a837ea9fe1 v4.1.0 - Memory optimization, Buy Me a Coffee button, and updated mobile Protocol.cs 2026-02-05 02:26:13 +02:00
Manos ac7cea2e4b Add Shell Terminal to NEW in v3.3.0 section 2026-01-26 01:12:10 +02:00
Manos abaa028d28 Update documentation for v3.3.0 - Smart Search & Shell Terminal 2026-01-26 01:09:29 +02:00
Manos 45afca892c Update CHANGELOG.md with versions 2.0.0 through 3.2.0 2026-01-24 01:38:00 +02:00
Manos f065b79b33 Update mobile screenshot 2026-01-24 01:29:29 +02:00
38 changed files with 17063 additions and 26 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Release executables (too large for git, uploaded as release assets)
release/**/PS5Upload.exe
+385
View File
@@ -1,5 +1,390 @@
# Changelog - PS5 Upload Suite
## Version 4.2.1 - Bugfix Release (February 8, 2026)
### 🐛 Bug Fixes
#### UI Scrolling Fix
- **Fixed connection panel eating screen space** - Added `MaxHeight` constraint (250px) and `ScrollViewer` to the connection/payload settings panel
- **Fixed header overflow** - Added `MaxHeight="120"` to header row
- **Fixed content area too small** - Added `MinHeight="200"` to main content area
- When the Auto-send Payload Expander was open, it consumed half the screen leaving no room for file lists
#### File Counting Fix
- **Fixed chunked files never counting as completed** - `fileChunkCounts` was set to the actual number of chunks (e.g., 4 for a 2GB file), but `UploadFileParallelAsync` handles all chunks internally as a single Task. The main loop only saw 1 task completion per file, so chunked files never reached their completion threshold
- Now correctly sets `fileChunkCounts = 1` per file since each file = 1 task
#### Progress Display Fix
- **Fixed progress appearing frozen during heavy uploads** - All UI updates used `DispatcherPriority.Background` (lowest priority), which got starved when 24 parallel connections flooded the dispatcher queue
- Upgraded `UpdateUploadStats` to `DispatcherPriority.Normal`
- Upgraded per-file progress bars to `DispatcherPriority.Render`
- Progress now updates smoothly even at 95%+ network saturation
---
## Version 4.2.0 - Game Mounter Integration (February 7, 2026)
### 🎮 Game Mounter Integration
- **Mount Games Button** - New green "🎮 Mount Games" button in the client UI
- One-click mounting of all uploaded games directly from the PS5 Upload Suite
- Button disabled when disconnected, shows "⏳ Mounting..." during operation
### 🔍 Multi-Path Game Scanning
- Scans **6 locations** for games:
- `/data/etaHEN/games` (internal storage)
- `/mnt/usb0/games` through `/mnt/usb3/games` (USB drives)
- `/mnt/ext0/games` (M.2 SSD)
- First pass counts total games, second pass processes them
- PS5 notifications show real-time progress: "Mounting 3/10 (30%) - Game Name"
### 🔄 Smart Mount Features
- **Duplicate Detection** - Same title ID across multiple drives mounted only once
- **Auto-Cleanup** - `auto_unmount_deleted_games()` removes stale mount entries
- **Already Mounted Detection** - Skips games that are already mounted via nullfs
- **sce_sys Validation** - Verifies game directory has valid `sce_sys/` before mounting
### 🔧 Payload Changes
- **New Command** - `CMD_MOUNT_GAMES` (0x30) added to protocol
- **nullfs Mount** - Games mounted to `/system_ex/app/<title_id>`
- **DRM Patching** - Automatic `applicationDrmType` fix in `param.json`
- **Metadata Copy** - `sce_sys` copied to `/user/app/` and `/user/appmeta/`
- **mount.lnk** - Tracks mount source path for cleanup and remount
- **Safe Delete** - Replaced `system("rm -rf ...")` with `rmdir_recursive()` to prevent hangs
- **remount_system_ex()** - Remounts `/system_ex` as writable for game installation
### 🖥️ Client Changes
- **Protocol.cs** - Added `MountGames = 0x30` enum and `MountGamesAsync()` method
- **MainWindow.xaml** - Green "🎮 Mount Games" button between Upload and Cancel
- **MainWindow.xaml.cs** - Click handler with progress display and auto-registration
- **Auto Registration** - If new games detected, automatically runs `game_mounter.elf` via shell for home screen registration
### ⚡ Client Improvements
- **Real-Time Speed Display** - New sliding window algorithm (10 samples over 5 seconds) for accurate real-time upload speed
- **Smoother ETA** - Blended speed calculation (60% real-time + 40% average) with better time formatting (hours/minutes/seconds)
- **Speed Display** - Shows both real-time and average speed: `Speed: 95 MB/s (avg 88 MB/s)`
- **Duplicate File Dialog** - New `DuplicateFileDialog` for handling file conflicts during upload
- **Move Dialog Refactor** - Replaced inline move dialog code with reusable `ShowPathPickerDialogAsync()`
- **FormatFileSize Refactor** - Moved to static `FileUtils.FormatFileSize()` class for code reuse
- **ETA Smoothing** - Reduced `ETASmoothingFactor` from 0.3 to 0.15 for less jumpy ETA display
### 🐛 Bug Fixes
#### Payload Fixes
- **Fixed IOVEC_ENTRY(NULL) crash** - `strlen(NULL)` with `-O3` optimization caused segfault in `remount_system_ex()`
- **Fixed libSceAppInstUtil startup crash** - Library cannot be loaded in etaHEN payload context (crashes dynamic linker). Switched to standalone `game_mounter.elf` for registration
- **Fixed dlopen crash** - `dlopen("libSceAppInstUtil.sprx")` crashes payload thread. Removed dynamic loading approach
- **Removed system() calls** - `system("rm -rf ...")` replaced with safe recursive `rmdir_recursive()` to prevent payload hangs
- **Fixed compile warnings** - Removed unused `#include <dlfcn.h>`, re-enabled `auto_unmount_deleted_games()`
#### Client Fixes
- **Fixed Shell Terminal hardcoded IP** - Shell was connecting to hardcoded `192.168.0.160` instead of the configured PS5 IP address
- **Fixed ETA jumping** - ETA no longer jumps erratically during uploads due to improved smoothing
- **Fixed speed dropping to 0** - Sliding window prevents speed display from showing 0 during brief pauses
- **Fixed files remaining counter** - Simplified calculation with `Math.Max(0, ...)` to prevent negative values
### 📝 Technical Notes
- `libSceAppInstUtil.sprx` is **not available** in etaHEN payload context (both static linking and dlopen crash)
- Game registration (`sceAppInstUtilAppInstallTitleDir`) is handled by the standalone `game_mounter.elf` which runs as a separate ELF with its own library dependencies
- The upload suite payload handles nullfs mounting, metadata copy, and DRM patching — everything except the final system registration
---
## Version 4.1.0 - Memory Optimization & Large File Handling (February 5, 2026)
### 🧠 Memory & Performance Improvements
#### 1. Large File Count Support (156K+ Files)
- **Fixed crash** with games containing 156,253+ files (e.g., Astrobot)
- **Removed massive dictionary** - No longer creates 156K FileInfo objects at once
- **Incremental calculation** - Total bytes calculated progressively during scan
- **Memory efficient** - Handles unlimited file counts without OutOfMemoryException
#### 2. Lock-Free Progress Tracking
- **ConcurrentDictionary** - Replaced Dictionary with thread-safe collections
- **Zero lock contention** - Eliminated `lock (_progressLock)` blocks
- **Atomic operations** - Uses `Interlocked.Add()` for progress updates
- **Smooth UI** - No stuttering from lock contention
#### 3. Auto Memory Cleanup
- **Completed files removed** - Dictionaries cleaned after file completion
- **Reduced memory footprint** - Only active uploads tracked
- **Prevents memory overflow** - Long-running uploads stay stable
#### 4. Reduced UI Stuttering
- **Batch logging** - Small files logged every 50 completions (up from 25)
- **Less UI pressure** - Fewer `Dispatcher.Invoke()` calls
- **Smoother experience** - Especially noticeable with 100K+ files
### 🔧 Technical Changes
- Changed `_fileProgressBytes` from Dictionary to ConcurrentDictionary
- Changed `_fileChunkProgressBytes` from Dictionary to ConcurrentDictionary
- Changed `_chunkLogLastBytes` from Dictionary to ConcurrentDictionary
- Removed `fileSizeLookup` dictionary creation (caused OutOfMemoryException)
- Increased `SmallFileLogBatchSize` from 25 to 50
- Added dictionary cleanup in file completion handlers
### 🐛 Bug Fixes
- Fixed application crash when uploading games with 156K+ files
- Fixed UI stuttering during small file uploads
- Fixed memory overflow from massive dictionary allocations
- Fixed lock contention causing UI freezes
### 📊 Tested With
- **Astrobot**: 156,253 files, 1,724 directories - **53 minutes total**
- **Result**: Zero crashes, smooth upload, stable memory usage
---
## Version 4.0.0 - Retry Feature & Stability Improvements (January 27, 2026)
### ✨ New Features
#### 🔄 Retry Failed Transfers
- **Right-click context menu** on failed transfers
- **Retry Upload** - Re-queue failed files for upload
- **Remove from List** - Clear individual failed items
- **Persistent paths** - Failed transfers store local and remote paths
- **Smart retry** - Validates file existence before retry
#### 🗑️ Clear All Button Fix
- **Transfer History Clear All** now works correctly
- **Confirmation dialog** before clearing history
- **Clears both** completed and failed transfers
### 🔧 Performance & Stability
#### Connection Stability
- **Reverted 16MB buffer** - Caused connection drops with 16 parallel connections
- **Stable 8MB buffer** - Proven reliable for high-speed transfers
- **TCP NoDelay** - Already enabled for optimal performance
- **16MB socket buffers** - Maintained for maximum throughput
#### Upload Performance
- **Large files (>100MB)**: 30-36 MB/s per file
- **Small files (<100MB)**: 8-12 MB/s per file
- **4 parallel large files** - Prevents PS5 memory exhaustion
- **16 total connections** - Optimal for mixed workloads
### 🐛 Bug Fixes
- Fixed Clear All button not clearing transfer history
- Fixed connection drops when using oversized buffers
- Added proper event handlers for failed transfer context menu
- Improved error handling for retry operations
### 📝 Code Quality
- Added comprehensive inline documentation
- Improved code organization and readability
- Better error messages for failed operations
- Enhanced UI feedback for user actions
---
## Version 3.3.0 - Smart Search & Enhanced Mobile (January 26, 2026)
### 🔍 Smart Search Feature
- **Full Filesystem Indexing** - Index entire PS5 filesystem (200,000+ files)
- **Instant Search** - Lightning-fast search with wildcards (`*.pkg`, `*loader*`)
- **Size Filters** - Search by file size (`size:>1GB`, `size:<100MB`)
- **Case-Insensitive** - Matches both filename and full path
- **Navigate from Search** - Double-click results to jump to folder (Desktop) / Tap to navigate (Mobile)
- **Index Status** - Real-time indexing progress with file/directory counts
- **Smart Indexing** - Skips problematic directories (`/dev`, `/proc`, `/sys`)
### 📱 Mobile App v1.2
- **🔍 Search Tab** - Full Smart Search functionality on Android
- **Navigate to Location** - Tap search results to navigate to folder
- **Copy Path** - Copy file paths to clipboard from search results
- **Connection Manager** - Singleton pattern for reliable IP/port management
- **Improved Navigation** - Fixed MainPage reference for seamless navigation
### 🖥️ Desktop Client Updates
- **Search Tab** - New dedicated tab for Smart Search
- **💻 Shell Terminal** - Execute commands directly on PS5
- Run system commands remotely
- Real-time output display
- Command history
- Working directory support
- **Equal Panel Layout** - Local Files, PS5 Files, and Right panel now equal width
- **Improved UI** - Index Status and buttons positioned above search box
- **Debounced Search** - Smooth typing experience with 300ms debounce
### 🔧 Payload Updates
- **Indexing Commands** - `CMD_INDEX_START` (0x40), `CMD_INDEX_STATUS` (0x41), `CMD_SEARCH_INDEX` (0x42)
- **In-Memory Index** - Linked list structure for fast search
- **Wildcard Matching** - Support for `*` and `?` wildcards
- **Thread-Safe** - Mutex-protected index operations
- **Root Path Support** - Can index from `/` (entire filesystem)
### 🐛 Bug Fixes
- Fixed indexing hangs when scanning root directory
- Fixed search protocol compatibility between mobile and desktop
- Fixed navigation stack issues in mobile app
- Fixed nullable reference warnings in mobile code
- Added proper error handling for index operations
---
## Version 3.2.1 - Privacy & Bug Fixes (January 24, 2026)
### 🔒 Privacy Improvements
- **Removed disk space display** - No longer shows PS5 storage capacity (Free/Used/Total)
- **Fixed storage query bug** - Resolved syntax errors in statvfs code that could cause crashes
- **Privacy-focused** - Application no longer queries or displays sensitive storage information
### 🐛 Bug Fixes
- Fixed potential crash from malformed storage query code
- Removed unused protocol busy flag that caused compiler warnings
- Cleaned up all storage-related UI elements and backend code
---
## Version 3.2.0 - Mobile Improvements & Bug Fixes (January 24, 2026)
### 🎯 Improvements
#### 💾 Accurate Storage Display
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
- **Accounts for reserved space** - Same calculation as PS5 system
- Uses proper calculation: `(total_blocks - reserved) * block_size`
#### 📱 Mobile App Improvements
- **Multi-select mode** - Toggle button (☑️/✅) to select multiple files/folders
- **Folder browser** - Visual folder picker for Copy/Move destinations with recursive navigation
- **Sorted file list** - Folders always displayed at top, then files (alphabetically)
- **Exit confirmation** - Prompt dialog when pressing back button
- **Delete folders** - Now supports folder deletion (not just files)
- **Batch operations** - Delete/download multiple items at once
#### 3. 🐛 Bug Fixes
- **Fixed empty folder deletion** - No more "Unexpected response" error when deleting empty directories
- **Fixed Copy/Move crash** - No longer crashes when selecting destination folder (saved file info before browsing)
- **Fixed multi-select count** - Correct item count in delete confirmation dialog
- **Fixed folder navigation** - Can enter folders after actions complete (auto-exit multi-select mode)
---
## Version 3.1.0 - Parallel Chunked Uploads (January 2026)
### 🚀 Performance Improvements
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
- **Large files (>100MB)** are now split into 500MB chunks
- **4 parallel connections** upload chunks simultaneously
- **Result:** ~104 MB/s upload speed (2x faster than before!)
- **Fixed race condition:** First chunk creates file before others start
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
#### 2. 📊 Real-time Progress Display
- **Speed display** now updates in real-time during chunked uploads
- **ETA display** shows accurate time remaining
- **Progress bar** correctly shows 0-100% for chunked files
- No more speed dropping to 0 during large file uploads
#### 3. 💾 Storage Display Improvements
- **Changed label** to "Data Storage (/data)" for clarity
- **Uses statfs with f_bavail** for more accurate available space
- **Real-time updates** every 5 seconds when connected
---
## Version 3.0.0 - Mobile Client & Path Fixes (January 2026)
### 🐛 Critical Bug Fixes
#### 1. 🔧 Path Normalization Fix
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
- **Solution:** Added `NormalizePath()` function in both client and server
- **Result:** 100% reliable directory creation and file uploads
#### 2. 📱 Android Mobile Client
- **NEW:** Full-featured Android app for PS5 file management
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
- Upload files from phone to PS5
- Download files from PS5 with Share option
- Browse PS5 filesystem
- Create new folders
- Rename files/folders
- Copy files/folders
- Move files/folders
- Delete files
- Favorites for quick navigation
- Debug Log with Copy to clipboard
- Transfer History tracking
#### 3. 🛠️ Server Improvements
- Path normalization in all handler functions
- More robust error handling
- Improved stability for parallel uploads
---
## Version 2.1.0 - Performance & History (January 2026)
### 🚀 Performance Optimizations
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
- **Per-file:** 11-14 MB/s sustained per connection
#### 2. 📊 Transfer History
- Complete history of all uploads and downloads
- Success/Failed status tracking with error messages
- Speed statistics (average, min, max)
- Export to CSV/JSON for analysis
- Persistent storage across sessions
#### 3. 🔄 Auto-Clear History on Startup
- Optional checkbox to clear history automatically
- Useful for keeping UI clean between sessions
- Setting saved in `ps5_upload_settings.json`
#### 4. 🖥️ Maximized Window UI
- Application opens in full-screen mode by default
- Better visibility for large file transfers
- Can be resized/restored as needed
### Technical Improvements
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
---
## Version 2.0.0 - Download & Multi-PS5 (January 2026)
### 🎉 4 Major New Features
#### 1. 📥 Download Files (PS5 → PC)
- Right-click any file → "⬇️ Download to PC"
- Save file dialog for destination selection
- Real-time progress tracking with speed display
- Optimized with sendfile for maximum speed
#### 2. 🔍 File Search
- Search box in PS5 Files panel
- Real-time filtering as you type
- Case-insensitive search
- Quick "Clear" button to reset
#### 3. ⭐ Favorites/Bookmarks
- Save frequently used PS5 paths
- Quick dropdown navigation
- Add/Remove favorite paths
- Persistent storage in JSON
#### 4. 🎮 Multi-PS5 Support
- Save multiple PS5 profiles (IP + name)
- Quick switch between different PS5 consoles
- Dropdown profile selector
- Persistent profile storage
---
## Version 1.3.0 - Stable Release (January 13, 2026)
### 🎯 Major Achievement
+221 -26
View File
@@ -1,12 +1,93 @@
# 🚀 PS5 Upload Suite - High-Speed File Transfer
# 🚀 PS5 Upload Suite - Complete PS5 Management Platform
**By Manos**
**Version 3.2.0 - Mobile Improvements & Bug Fixes**
**Version 5.0.0 - Complete Management Suite**
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **104+ MB/s** upload speeds for large files using parallel chunked uploads!
Complete all-in-one PS5 management platform with **104+ MB/s** high-speed file transfers, live hardware monitoring, full game/save/screenshot management, and deep PS5 integration — all through a modern desktop client and a robust PS5 payload.
⭐ **NEW in v3.2.0:** Accurate storage display (848 GB), mobile multi-select, folder browser, bug fixes!
---
⭐ **NEW in v5.0.0 (Complete Management Suite):**
### 🖥️ **NEW: Hardware Monitor Tab**
- 📋 **System Info** — Model (CFI-xxxx), Serial Number, Architecture, OS version
- 💾 **Physical RAM** — With 5-step detection fallback (PS5-specific API + sysctl chain + 16 GB default)
- 🔥 **Live Sensors** — CPU temperature, SoC temperature, CPU frequency, SoC power consumption
- 🔄 **Auto-refresh** — Configurable 5-second polling with automatic disconnect detection
- 🛡️ **Payload Hardened** — Mutex + 1s cache for sensors, 5s throttle for power API, rejection of abnormal readings
### 📷 **NEW: Screenshots Tab**
- 🖼️ **Visual Gallery** — Live thumbnail previews (200px downsampled for fast loading)
- 💾 **Smart Cache** — Local cache in `%TEMP%` keyed by remote path hash — zero re-downloads on refresh
- ⬇️ **Batch Download** — Multi-select and download multiple screenshots
- 🗑️ **Clean Delete** — Removes ALL 3 files per screenshot (`.dat`, `.meta`, `.jpg.jpeg`) — no more ghost entries in PS5 gallery
- 👁️ **Double-click Preview** — Opens in default image viewer
- 🔍 **Full Path Scanning** — 5-level recursive scan of `/user/av_contents/thumbnails/photo/…`
### ▶️ **NEW: Launch Game**
- Right-click any mounted game → **Launch Game**
- Uses `sceLncUtilLaunchApp` (low-level) with fallback to `sceSystemServiceLaunchApp`
- Works on mounted/sideloaded games (bypasses `0x80940005` permission error)
- Triple-strategy launcher with detailed diagnostic error messages
### 💾 **NEW: Save Manager Tab**
- **Full Save Browsing** — Per-game save data with metadata
- **Game Icons** — Fetched from PSN/local param.sfo (cached)
- **Download Saves** — Batch-download multiple saves to local folder
- **Upload Saves** — Drag & drop save files back to PS5
- **Save Details** — Size, modification time, title ID, path
### 🎮 **NEW: Game Details Window**
- **PSN Cover Art** — Official game covers fetched from PlayStation Store
- **Full Metadata** — param.json contents formatted nicely
- **Online PSN Info** — Description, genres, age rating, publisher
- **Alternate Title ID detection** — Auto-fallback for regional variants
- **Browser Search Fallback** — Opens PSN store search if API fails
### 🔧 **Improved Storage Reporting**
- ✅ Matches **PS5 Settings "Console Storage"** formula exactly
- ✅ Total: `/user` effective + `/system_data` + `/system_ex`
- ✅ Free: proper `f_bavail` across all partitions
- ✅ Accounts for reserved space & per-partition blocksize
- Example real-world PS5: **848 GB total / 354 GB free** ✓
### 🎮 **Improved Mount Games**
- ✅ Correct `sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0)` signature
- ✅ Registration BEFORE `mount.lnk` (correct order)
- ✅ Direct in-payload registration (no external `game_mounter.elf` dependency)
- ✅ Proper DRM patching & metadata copy
### 🛡️ **Stability Overhaul**
- ✅ **Thread-safe sensor reads** — `pthread_mutex_t` prevents concurrent API crashes
- ✅ **API call throttling** — SoC power read at most every 5s, sensor cache every 1s
- ✅ **Cached static HW info** — Read once, serve forever (Model, Serial, OS)
- ✅ **Unsafe APIs removed** — `sceKernelIccGet*`, `HwHasWlanBt`, `HwHasOpticalOut` (caused crashes)
- ✅ **Client busy-flag guard** — `Interlocked` prevents overlapping refreshes
- ✅ **Auto-stop timers** — Hardware auto-refresh stops on disconnect
- ✅ **Graceful reconnect** — Connection lost → automatic reconnection attempt
---
⭐ **v4.2.2 (Stability & Mount Fix):**
- 🎮 **Fixed Mount Games** - Games now properly appear on PS5 home screen after mounting
- 🗑️ **Fixed Unmount Games** - Games now properly removed from PS5 home screen when unmounted
- 🖥️ **Fixed Hardware Tab** - Added ScrollViewer for proper scrolling
- 🔧 **Fixed sceAppInstUtil calls** - Correct API usage with proper arguments
- ⚠️ **Disabled auto hardware refresh** - Prevents payload crashes (manual refresh still available)
- 🛡️ **Improved stability** - Fixed delayed payload crashes during folder navigation
⭐ **v4.2.1 (Bugfix):**
- 🖥️ **Fixed UI scrolling** - Connection panel no longer eats half the screen
- 🔢 **Fixed file counting** - Chunked files now count correctly as completed
- 📊 **Fixed frozen progress** - Progress updates no longer freeze during heavy uploads
⭐ **v4.2.0 Features:**
- 🎮 **Mount Games Button** - Mount uploaded games directly from the client
- 🔍 **Multi-Path Scanning** - Scans internal storage, USB drives (0-3), and M.2 SSD
- 🔄 **Duplicate Detection** - Skips already-mounted games automatically
- 📊 **Mount Summary** - Detailed results with PS5 notifications
- 🧹 **Auto-Cleanup** - Removes mount entries for deleted games
📱 **Android Mobile Client available!**
@@ -28,12 +109,18 @@ Download the latest release from the [Releases](https://github.com/manos555555/P
### 2. Windows GUI Client
- **File:** `PS5Upload.exe`
- Self-contained executable (no .NET installation required)
- Modern dark theme UI
- Self-contained executable (no .NET installation required, **~147 MB**)
- Modern dark theme UI with multi-tab layout
- **Tabs:** Debug Log, Transfer History, Shell, Search, Saves, Games, Screenshots, **Hardware 🆕**
- Drag & drop file/folder upload
- Real-time upload progress with speed tracking
- Transfer History with success/failed tracking
- Storage space display
- Real-time upload progress with sliding window speed tracking
- Transfer History with success/failed tracking and retry
- PS5 storage display (free/used/total — matches PS5 Settings)
- Game mounting with one-click Mount Games button
- **NEW:** Launch any mounted game with one click
- **NEW:** Live hardware monitor (CPU/SoC temps, frequency, power)
- **NEW:** Screenshot gallery with thumbnail previews and batch download/delete
- **NEW:** Save Manager with game icons and PSN metadata
### 3. 📱 Android Mobile Client
- **File:** `PS5UploadMobile.apk`
@@ -72,27 +159,94 @@ Download the latest release from the [Releases](https://github.com/manos555555/P
## 🎨 Features
### PS5 Server
✅ **High-Speed Protocol** - Custom binary protocol
✅ **4MB Buffers** - Maximum throughput
✅ **Direct Disk I/O** - No temp files
✅ **Multi-threaded** - Handle multiple clients
✅ **Robust Error Handling** - Graceful failures
### PS5 Server Payload
✅ **High-Speed Protocol** - Custom binary protocol for maximum speed
✅ **8MB Buffer** - Large buffer for high throughput
✅ **16MB Socket Buffers** - Maximum network throughput
✅ **Direct Disk I/O** - No temp files, direct write syscalls
✅ **Multi-threaded** - Handle multiple clients simultaneously
✅ **Per-File Mutex** - Safe parallel uploads without corruption
✅ **Thread-safe Sensor Reads** - `pthread_mutex_t` around all kernel API calls
✅ **Smart Caching** - 1s sensor cache, 5s power-API throttle, permanent HW info cache
✅ **5-Minute Socket Timeout** - No connection drops on large files
✅ **Game Mounting** - nullfs mount, DRM patching, metadata copy, `sceAppInstUtil` registration
✅ **Game Launching** - `sceLncUtilLaunchApp` + fallback chain for pirated/mounted games
✅ **Shell Commands** - Built-in shell with ls, cd, cat, mkdir, rm, etc.
✅ **Filesystem Indexing** - In-memory index for instant search
✅ **Screenshot Management** - Full scan + safe delete of PS5 media files (.dat + .meta + .jpg.jpeg)
✅ **Hardware APIs** - Model, serial, temps, CPU freq, power via verified Sony kernel APIs
✅ **Storage Matching PS5 Settings** - Aggregates /user + /system_data + /system_ex
### Windows Client
✅ **Modern UI** - Dark theme, maximized window for better visibility
✅ **Modern UI** - Dark theme, maximized window, 8+ functional tabs
✅ **Drag & Drop** - Files and folders
✅ **Browse PS5** - Navigate filesystem
✅ **Real-time Progress** - Speed & percentage tracking
✅ **Optimized Upload** - 8 parallel connections for 88-110 MB/s aggregate speed (NEW v2.1)
✅ **Transfer History** - Track all uploads/downloads with success/failed status (NEW v2.1)
✅ **Auto-Clear History** - Optional auto-clear on startup (NEW v2.1)
✅ **Browse PS5** - Navigate filesystem with favorites
✅ **Real-time Speed** - Sliding window algorithm for accurate speed display
✅ **Parallel Uploads** - 4 parallel chunked connections for 104+ MB/s
✅ **Transfer History** - Track all uploads/downloads with success/failed status
✅ **Retry Failed Transfers** - Right-click to retry failed uploads
✅ **Duplicate File Dialog** - Handle file conflicts during upload
✅ **Folder Upload** - Recursive directory upload
✅ **Storage Info** - Free space display
✅ **Download Files** - Download from PS5 to PC (v2.0)
✅ **File Search** - Real-time filtering of PS5 files (v2.0)
✅ **Favorites/Bookmarks** - Quick navigation to saved paths (v2.0)
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles (v2.0)
✅ **Download Files** - Download from PS5 to PC
✅ **💾 Storage Display** - Real-time PS5 storage info (matches PS5 Settings exactly)
✅ **🔍 Smart Search** - Full filesystem indexing with instant search
- Wildcard support: `*.pkg`, `*loader*`, `game*.bin`
- Size filters: `size:>1GB`, `size:<100MB`
- Case-insensitive matching
- Search both filename and full path
- Double-click to navigate to folder
✅ **💻 Shell Terminal** - Execute commands directly on PS5
- Run system commands remotely
- Real-time output display
- Command history
- Working directory support
✅ **⭐ Favorites/Bookmarks** - Quick navigation to saved paths
✅ **👥 Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
✅ **🎮 Mount Games** - Mount uploaded games directly from the client
- Scans all game paths: internal, USB drives (0-3), M.2 SSD
- Duplicate detection across storage locations
- Auto-cleanup of deleted game mounts
- PS5 notifications with mount progress
- In-payload registration (no external binary required)
✅ **▶️ Launch Games** - One-click launch for mounted games
- `sceLncUtilLaunchApp` low-level launcher
- Works on pirated/sideloaded games
- Fallback to SystemService API
- Right-click context menu on any game
✅ **💾 Save Manager Tab** - Full PS5 save file management
- Per-game save browsing with thumbnails
- Game icons fetched and cached from PSN/param.sfo
- Batch download saves to local backup folder
- Upload saves back to PS5
- Size, date, and metadata display
✅ **🎮 Game Details Window** - Rich game information
- PSN cover art (fetched from PlayStation Store)
- Full param.json metadata formatted
- Online PSN description, genres, age rating, publisher
- Alternate title ID detection (regional variants)
- Browser search fallback
✅ **📷 Screenshots Tab** - Complete media gallery
- Visual thumbnail previews (downsampled 200px)
- Local cache in %TEMP% (reuses across refreshes)
- Batch download/delete multi-select
- Double-click preview in default image viewer
- Clean delete removes ALL 3 PS5 files (.dat + .meta + .jpg.jpeg)
- No more ghost entries in PS5 Media Gallery
✅ **🖥️ Hardware Monitor Tab** - Live system stats
- **System Info:** Model (CFI-xxxx), Serial, Architecture, OS, CPU Cores, RAM, Wi-Fi/BT
- **Live Sensors:** CPU temp, SoC temp, CPU frequency, SoC power consumption
- Auto-refresh with configurable 5s interval
- Busy-flag guard prevents overlapping refreshes
- Auto-stops on disconnect
- Progress bars for visual feedback
---
@@ -107,6 +261,12 @@ Download the latest release from the [Releases](https://github.com/manos555555/P
- ✅ **Fully responsive UI** throughout upload
- ✅ **No memory leaks** - stable operation
### v4.1.0 Real-World Test (Astrobot):
- ✅ **156,253 files** uploaded successfully in **53 minutes**
- ✅ **1,724 directories** created
- ✅ **Zero crashes** - stable memory usage throughout
- ✅ **Smooth UI** - no stuttering with massive file counts
### Previous Results (v1.3.0):
- ✅ **42,801 files** uploaded successfully
- ✅ **60-150 MB/s** sustained throughput
@@ -150,6 +310,41 @@ Download the latest release from the [Releases](https://github.com/manos555555/P
---
## 📝 What's New in v4.2.0
### 🎮 Game Mounter Integration
- **Mount Games Button** - New green "🎮 Mount Games" button in the client UI
- **Multi-Path Scanning** - Scans 6 locations: `/data/etaHEN/games`, USB drives (0-3), M.2 SSD (`/mnt/ext0/games`)
- **Duplicate Detection** - If the same game exists on multiple drives, it's mounted only once
- **Auto-Cleanup** - Automatically unmounts entries for games that have been deleted
- **PS5 Notifications** - Real-time progress notifications on PS5 screen during mounting
- **Detailed Summary** - Shows new mounts, already mounted, duplicates skipped, and failures
- **Auto Registration** - Automatically runs `game_mounter.elf` for home screen registration when new games are detected
### 🔧 Payload Improvements
- **New Protocol Command** - `CMD_MOUNT_GAMES` (0x30) for triggering game mount from client
- **nullfs Mounting** - Games mounted via nullfs to `/system_ex/app/`
- **DRM Patching** - Automatic DRM type fix in param.json
- **sce_sys Copy** - Copies game metadata to `/user/app/` and `/user/appmeta/`
- **mount.lnk Tracking** - Creates mount link files for persistent mount state
### ⚡ Client Improvements
- **Real-Time Speed Display** - Sliding window algorithm for accurate real-time upload speed
- **Smoother ETA** - Blended speed calculation (60% real-time + 40% average) with better formatting
- **Duplicate File Dialog** - New dialog for handling file conflicts during upload
- **Move Dialog Refactor** - Reusable `ShowPathPickerDialogAsync()` replaces inline code
### 🐛 Bug Fixes
- **Fixed IOVEC_ENTRY(NULL) crash** - `strlen(NULL)` with `-O3` optimization caused payload crash
- **Fixed libSceAppInstUtil crash** - Library not available in etaHEN payload context, switched to standalone registration
- **Removed system() calls** - Replaced `system("rm -rf ...")` with safe recursive delete to prevent hangs
- **Fixed Shell Terminal hardcoded IP** - Shell was connecting to hardcoded IP instead of configured PS5 address
- **Fixed ETA jumping** - ETA no longer jumps erratically during uploads
- **Fixed speed dropping to 0** - Sliding window prevents speed display from showing 0 during pauses
- **Fixed files remaining counter** - Prevented negative values in remaining files display
---
## 📝 What's New in v3.2.0
### 🎯 Improvements:
+8
View File
@@ -0,0 +1,8 @@
<Application x:Class="PS5Upload.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
+61
View File
@@ -0,0 +1,61 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace PS5Upload
{
public partial class App : Application
{
private static readonly string LogFilePath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
$"ps5upload_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
private static readonly object _logLock = new object();
public App()
{
// Global exception handlers
this.DispatcherUnhandledException += App_DispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
LogToFile("=== Application Started ===");
}
private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
LogToFile($"DISPATCHER EXCEPTION: {e.Exception}");
e.Handled = true; // Prevent crash, but log it
MessageBox.Show($"Error: {e.Exception.Message}\n\nCheck log file: {LogFilePath}",
"PS5 Upload Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var ex = e.ExceptionObject as Exception;
LogToFile($"UNHANDLED EXCEPTION: {ex}");
}
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
LogToFile($"TASK EXCEPTION: {e.Exception}");
e.SetObserved(); // Prevent crash
}
public static void LogToFile(string message)
{
try
{
lock (_logLock)
{
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
File.AppendAllText(LogFilePath, $"[{timestamp}] {message}\n");
}
}
catch { /* Ignore logging errors */ }
}
}
}
+79
View File
@@ -0,0 +1,79 @@
<Window x:Class="PS5Upload.DuplicateFileDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="File Already Exists"
Height="320"
Width="550"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Background="#1E1E1E">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="#2D2D30"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#3F3F46"/>
<Setter Property="Padding" Value="15,8"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Cursor" Value="Hand"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3E3E42"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Icon and Message -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,15">
<TextBlock Text="⚠️" FontSize="32" Margin="0,0,15,0"/>
<TextBlock x:Name="MessageTextBlock"
Text="This file already exists on PS5:"
Foreground="White"
FontSize="14"
VerticalAlignment="Center"/>
</StackPanel>
<!-- File Name -->
<TextBlock Grid.Row="1"
x:Name="FileNameTextBlock"
Foreground="#4EC9B0"
FontSize="13"
FontWeight="Bold"
Margin="0,0,0,10"
TextWrapping="Wrap"/>
<!-- File Info -->
<StackPanel Grid.Row="2" Margin="0,0,0,20">
<TextBlock x:Name="LocalSizeTextBlock"
Foreground="#9CDCFE"
FontSize="12"
Margin="0,0,0,5"/>
<TextBlock x:Name="RemoteSizeTextBlock"
Foreground="#9CDCFE"
FontSize="12"/>
</StackPanel>
<!-- Buttons -->
<StackPanel Grid.Row="4" Orientation="Vertical">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,10">
<Button Content="Replace" Width="120" Click="Replace_Click"/>
<Button Content="Skip" Width="120" Click="Skip_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Replace All" Width="120" Click="ReplaceAll_Click"/>
<Button Content="Skip All" Width="120" Click="SkipAll_Click"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
+67
View File
@@ -0,0 +1,67 @@
using System.Windows;
namespace PS5Upload
{
public partial class DuplicateFileDialog : Window
{
public enum FileAction
{
Replace,
Skip,
ReplaceAll,
SkipAll
}
public FileAction UserAction { get; private set; }
public DuplicateFileDialog(string fileName, long localSize, long remoteSize)
{
InitializeComponent();
FileNameTextBlock.Text = fileName;
LocalSizeTextBlock.Text = $"Local file: {FormatFileSize(localSize)}";
RemoteSizeTextBlock.Text = $"Remote file: {FormatFileSize(remoteSize)}";
}
private void Replace_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.Replace;
DialogResult = true;
Close();
}
private void Skip_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.Skip;
DialogResult = true;
Close();
}
private void ReplaceAll_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.ReplaceAll;
DialogResult = true;
Close();
}
private void SkipAll_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.SkipAll;
DialogResult = true;
Close();
}
private string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
}
+188
View File
@@ -0,0 +1,188 @@
<Window x:Class="PS5Upload.GameDetailsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Game Details"
Height="720" Width="860"
Background="#1E1E1E"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize">
<Grid>
<!-- Cover Art Background (pic0.png) -->
<Image x:Name="BackgroundCoverImage"
Stretch="UniformToFill"
Opacity="0.25"
RenderOptions.BitmapScalingMode="HighQuality"/>
<!-- Gradient overlay for readability -->
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#CC1E1E1E" Offset="0"/>
<GradientStop Color="#E61E1E1E" Offset="0.5"/>
<GradientStop Color="#FF1E1E1E" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Header: icon + title -->
<Border Grid.Row="0" Background="#CC2D2D30" CornerRadius="6" Padding="15">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="128" Height="128" CornerRadius="8"
Background="#3E3E42" ClipToBounds="True" Margin="0,0,20,0">
<Grid>
<TextBlock Text="🎮" FontSize="64"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Image x:Name="GameIconImage"
Stretch="UniformToFill"
RenderOptions.BitmapScalingMode="HighQuality"/>
</Grid>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock x:Name="GameNameText"
Text="Game Name"
FontSize="22" FontWeight="Bold"
Foreground="White"
TextTrimming="CharacterEllipsis"/>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock x:Name="TitleIdText"
Text="TITLE_ID"
FontSize="14" FontWeight="SemiBold"
Foreground="#007ACC"/>
<TextBlock Text=" • " Foreground="#666666" FontSize="14"/>
<TextBlock x:Name="RegionText"
Text="Region"
FontSize="14" Foreground="#FFC107"/>
<TextBlock Text=" • " Foreground="#666666" FontSize="14"/>
<TextBlock x:Name="StatusText"
Text="Status"
FontSize="14" Foreground="#28A745"/>
</StackPanel>
</StackPanel>
</Grid>
</Border>
<!-- Details panel -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0,15,0,15">
<StackPanel>
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="📊 Size &amp; Storage" FontSize="14" FontWeight="Bold"
Foreground="#9CDCFE" Margin="0,0,0,8"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Total size:"
Foreground="#999999" Margin="0,0,0,4"/>
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="TotalSizeText"
Text="-" Foreground="White" FontWeight="SemiBold"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="eboot.bin:"
Foreground="#999999" Margin="0,0,0,4"/>
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="EbootSizeText"
Text="-" Foreground="White"/>
</Grid>
</StackPanel>
</Border>
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="📁 Paths" FontSize="14" FontWeight="Bold"
Foreground="#9CDCFE" Margin="0,0,0,8"/>
<TextBlock Text="Source path:" Foreground="#999999"/>
<TextBlock x:Name="SourcePathText" Text="-"
Foreground="White" TextWrapping="Wrap" Margin="0,2,0,8"/>
<TextBlock Text="Install date:" Foreground="#999999"/>
<TextBlock x:Name="InstallDateText" Text="-"
Foreground="White" Margin="0,2,0,0"/>
</StackPanel>
</Border>
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15" Margin="0,0,0,10">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="🌐 Online Info (PSN Store)" FontSize="14" FontWeight="Bold"
Foreground="#9CDCFE" VerticalAlignment="Center"/>
<Button Grid.Column="1" x:Name="FetchPsnInfoButton"
Content="🔍 Fetch from Internet"
Click="FetchPsnInfoButton_Click"
Background="#007ACC" Foreground="White"
Padding="10,4" BorderThickness="0"/>
</Grid>
<TextBlock x:Name="PsnStatusText" Text="Click the button above to fetch online info"
Foreground="#888888" FontStyle="Italic" Margin="0,6,0,0"
TextWrapping="Wrap"/>
<StackPanel x:Name="PsnInfoPanel" Visibility="Collapsed" Margin="0,8,0,0">
<TextBlock x:Name="PsnTitleText" FontSize="15" FontWeight="Bold"
Foreground="White" Margin="0,0,0,4" TextWrapping="Wrap"/>
<TextBlock x:Name="PsnPublisherText" Foreground="#9CDCFE" Margin="0,0,0,4"/>
<TextBlock x:Name="PsnReleaseDateText" Foreground="#FFC107" Margin="0,0,0,6"/>
<ScrollViewer MaxHeight="180" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<TextBlock x:Name="PsnDescriptionText" Foreground="#CCCCCC"
TextWrapping="Wrap"/>
</ScrollViewer>
</StackPanel>
<Button x:Name="OpenInBrowserButton"
Content="🌍 Search on PSN Store (browser)"
Click="OpenInBrowserButton_Click"
Background="#3E3E42" Foreground="White"
Padding="10,4" BorderThickness="0"
HorizontalAlignment="Left"
Margin="0,10,0,0"/>
</StackPanel>
</Border>
<Border Background="#CC2D2D30" CornerRadius="6" Padding="15">
<StackPanel>
<TextBlock Text="📜 param.json" FontSize="14" FontWeight="Bold"
Foreground="#9CDCFE" Margin="0,0,0,8"/>
<TextBox x:Name="ParamJsonText"
IsReadOnly="True"
TextWrapping="Wrap"
Background="#1E1E1E"
Foreground="#CCCCCC"
BorderThickness="1"
BorderBrush="#3E3E42"
Padding="8"
FontFamily="Consolas"
FontSize="11"
MinHeight="120"
MaxHeight="240"
VerticalScrollBarVisibility="Auto"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- Footer buttons -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Close" Click="CloseButton_Click"
Background="#007ACC" Foreground="White"
Padding="20,8" BorderThickness="0"/>
</StackPanel>
</Grid>
</Grid>
</Window>
+496
View File
@@ -0,0 +1,496 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows;
namespace PS5Upload
{
public partial class GameDetailsWindow : Window
{
private readonly PS5MountedGame _game;
private readonly PS5Protocol? _protocol;
private readonly string _paramJsonRaw;
// Pre-configured HttpClient (properties locked after first use, set once here)
private static readonly HttpClient _http = CreateHttpClient();
private static HttpClient CreateHttpClient()
{
var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
client.Timeout = TimeSpan.FromSeconds(8);
return client;
}
public GameDetailsWindow(PS5MountedGame game, Dictionary<string, string> details, PS5Protocol? protocol = null)
{
InitializeComponent();
_game = game;
_protocol = protocol;
_paramJsonRaw = details.TryGetValue("param_json", out var pj) ? pj : "";
// Header
if (game.Icon != null)
{
GameIconImage.Source = game.Icon;
}
GameNameText.Text = details.TryGetValue("name", out var n) ? n : game.Name;
TitleIdText.Text = game.TitleId;
RegionText.Text = details.TryGetValue("region", out var r) ? r : game.Region;
bool isActive = details.TryGetValue("is_active", out var act) && act == "1";
StatusText.Text = isActive ? "✓ Mounted" : "✗ Not Mounted";
StatusText.Foreground = isActive
? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x28, 0xA7, 0x45))
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xDC, 0x35, 0x45));
// Sizes
if (details.TryGetValue("total_size", out var ts) && ulong.TryParse(ts, out ulong totalBytes))
{
TotalSizeText.Text = FormatSize(totalBytes);
}
if (details.TryGetValue("eboot_size", out var es) && ulong.TryParse(es, out ulong ebootBytes))
{
EbootSizeText.Text = FormatSize(ebootBytes);
}
// Paths
SourcePathText.Text = details.TryGetValue("path", out var p) ? p : game.Path;
InstallDateText.Text = details.TryGetValue("install_date", out var d) ? d : "Unknown";
// param.json
if (details.TryGetValue("param_json", out var param))
{
ParamJsonText.Text = FormatJson(param);
}
else
{
ParamJsonText.Text = "(not available)";
}
// Load cover art async (pic0.png)
_ = LoadCoverArtAsync();
}
private async Task LoadCoverArtAsync()
{
if (_protocol == null || !_protocol.IsConnected) return;
try
{
var picBytes = await _protocol.GetGamePicAsync(_game.TitleId, 0);
if (picBytes == null || picBytes.Length == 0) return;
Dispatcher.Invoke(() =>
{
try
{
var bitmap = new System.Windows.Media.Imaging.BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
bitmap.StreamSource = new MemoryStream(picBytes);
bitmap.EndInit();
bitmap.Freeze();
BackgroundCoverImage.Source = bitmap;
}
catch { }
});
}
catch { }
}
private void OpenInBrowserButton_Click(object sender, RoutedEventArgs e)
{
try
{
string query = !string.IsNullOrWhiteSpace(_game.Name) && _game.Name != "Unknown"
? _game.Name
: _game.TitleId;
string url = $"https://store.playstation.com/en-gb/search/{Uri.EscapeDataString(query)}";
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
catch (Exception ex)
{
MessageBox.Show($"Could not open browser: {ex.Message}", "Error",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private async void FetchPsnInfoButton_Click(object sender, RoutedEventArgs e)
{
FetchPsnInfoButton.IsEnabled = false;
PsnStatusText.Text = "Fetching from PSN Store...";
PsnInfoPanel.Visibility = Visibility.Collapsed;
try
{
var info = await FetchPsnInfoAsync(_game.TitleId);
if (info == null)
{
PsnStatusText.Text = "❌ Not found on PSN Store (game may be pirated/unreleased/region-locked). Try the browser search button below.";
return;
}
PsnTitleText.Text = info.Title;
PsnPublisherText.Text = string.IsNullOrEmpty(info.Publisher) ? "" : $"🏢 {info.Publisher}";
PsnReleaseDateText.Text = string.IsNullOrEmpty(info.ReleaseDate) ? "" : $"📅 Released: {info.ReleaseDate}";
PsnDescriptionText.Text = info.Description ?? "";
PsnInfoPanel.Visibility = Visibility.Visible;
PsnStatusText.Text = $"✓ Found on PSN Store ({info.Source})";
}
catch (Exception ex)
{
PsnStatusText.Text = $"❌ Error: {ex.Message}";
}
finally
{
FetchPsnInfoButton.IsEnabled = true;
}
}
private class PsnGameInfo
{
public string Title { get; set; } = "";
public string Publisher { get; set; } = "";
public string ReleaseDate { get; set; } = "";
public string Description { get; set; } = "";
public string Source { get; set; } = "";
}
private async Task<PsnGameInfo?> FetchPsnInfoAsync(string titleId)
{
// HttpClient is already configured in the static initializer (UserAgent + Timeout)
// Build list of IDs to try: primary title_id first, then alternates from param.json
var idsToTry = new List<string> { titleId + "_00" };
// Extract alternate IDs from param.json serviceIdForSharing field
// Format examples: "UP4572-PPSA01768_00", "UP4572-CUSA12650_00", "UP4572-PPSA16388_00-00000000000HWOAD"
var alternateIds = ExtractAlternateTitleIds(_paramJsonRaw);
foreach (var altId in alternateIds)
{
if (!idsToTry.Contains(altId)) idsToTry.Add(altId);
}
// Strategy 1: Chihiro API (titlecontainer) - VERIFIED working (returns full game data)
string[] chihiroRegions = { "US/en", "GB/en", "DE/de", "FR/fr", "JP/ja", "IT/it", "ES/es" };
foreach (var tryId in idsToTry)
{
foreach (var region in chihiroRegions)
{
try
{
var parts = region.Split('/');
string url = $"https://store.playstation.com/store/api/chihiro/00_09_000/titlecontainer/{parts[0]}/{parts[1]}/999/{tryId}";
var response = await _http.GetAsync(url);
if (!response.IsSuccessStatusCode) continue;
string json = await response.Content.ReadAsStringAsync();
if (json.Length < 50) continue;
var info = ParsePsnJson(json);
if (info != null)
{
string tag = tryId == titleId + "_00" ? "" : $" via alt ID {tryId}";
info.Source = $"Chihiro API ({region}){tag}";
return info;
}
}
catch { }
}
}
// Strategy 2: Valkyrie API resolve endpoint (newer, for newer games)
string[] valkyrieRegions = { "gb/GB", "us/US", "de/DE", "fr/FR", "ja/JP", "it/IT", "es/ES" };
foreach (var region in valkyrieRegions)
{
try
{
string url = $"https://store.playstation.com/valkyrie-api/{region.Split('/')[0]}/{region.Split('/')[1]}/19/resolve/{titleId}_00";
var response = await _http.GetAsync(url);
if (!response.IsSuccessStatusCode) continue;
string json = await response.Content.ReadAsStringAsync();
if (json.Length < 50) continue;
var info = ParseValkyrieJson(json);
if (info != null)
{
info.Source = $"Valkyrie API ({region})";
return info;
}
}
catch { }
}
// Strategy 3: Search by game name on Valkyrie search API (useful for pirated games with real title)
string gameName = _game.Name;
if (!string.IsNullOrWhiteSpace(gameName) && gameName != "Unknown")
{
try
{
string encoded = Uri.EscapeDataString(gameName);
string url = $"https://store.playstation.com/valkyrie-api/en/US/19/tumbler-search/{encoded}?suggested_size=5&mode=game";
var response = await _http.GetAsync(url);
if (response.IsSuccessStatusCode)
{
string json = await response.Content.ReadAsStringAsync();
var info = ParseValkyrieSearchJson(json, gameName);
if (info != null)
{
info.Source = "Valkyrie Search (by name)";
return info;
}
}
}
catch { }
}
return null;
}
private static PsnGameInfo? ParseValkyrieJson(string json)
{
try
{
using var doc = System.Text.Json.JsonDocument.Parse(json);
var root = doc.RootElement;
// Valkyrie format: { "included": [ { "attributes": { "name", "long-description", "release-date", ... } } ] }
if (!root.TryGetProperty("included", out var included)) return null;
if (included.ValueKind != System.Text.Json.JsonValueKind.Array) return null;
foreach (var item in included.EnumerateArray())
{
if (!item.TryGetProperty("attributes", out var attrs)) continue;
var info = new PsnGameInfo();
if (attrs.TryGetProperty("name", out var name))
info.Title = name.GetString() ?? "";
if (attrs.TryGetProperty("long-description", out var ld))
info.Description = StripHtml(ld.GetString() ?? "");
else if (attrs.TryGetProperty("description", out var sd))
info.Description = StripHtml(sd.GetString() ?? "");
if (attrs.TryGetProperty("release-date", out var rd))
{
string raw = rd.GetString() ?? "";
if (DateTime.TryParse(raw, out DateTime dt))
info.ReleaseDate = dt.ToString("yyyy-MM-dd");
else
info.ReleaseDate = raw;
}
if (attrs.TryGetProperty("provider-name", out var pub))
info.Publisher = pub.GetString() ?? "";
else if (attrs.TryGetProperty("publisher-name", out var pub2))
info.Publisher = pub2.GetString() ?? "";
if (!string.IsNullOrWhiteSpace(info.Title))
return info;
}
}
catch { }
return null;
}
private static PsnGameInfo? ParseValkyrieSearchJson(string json, string searchName)
{
try
{
using var doc = System.Text.Json.JsonDocument.Parse(json);
var root = doc.RootElement;
if (!root.TryGetProperty("included", out var included)) return null;
if (included.ValueKind != System.Text.Json.JsonValueKind.Array) return null;
PsnGameInfo? bestMatch = null;
foreach (var item in included.EnumerateArray())
{
if (!item.TryGetProperty("type", out var type)) continue;
if (type.GetString() != "game") continue;
if (!item.TryGetProperty("attributes", out var attrs)) continue;
if (!attrs.TryGetProperty("name", out var nameProp)) continue;
string name = nameProp.GetString() ?? "";
if (string.IsNullOrWhiteSpace(name)) continue;
// Find best match by name similarity
if (string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase) ||
name.Contains(searchName, StringComparison.OrdinalIgnoreCase) ||
searchName.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var info = new PsnGameInfo { Title = name };
if (attrs.TryGetProperty("long-description", out var ld))
info.Description = StripHtml(ld.GetString() ?? "");
if (attrs.TryGetProperty("release-date", out var rd))
{
string raw = rd.GetString() ?? "";
if (DateTime.TryParse(raw, out DateTime dt))
info.ReleaseDate = dt.ToString("yyyy-MM-dd");
}
if (attrs.TryGetProperty("provider-name", out var pub))
info.Publisher = pub.GetString() ?? "";
// Exact match wins immediately
if (string.Equals(name, searchName, StringComparison.OrdinalIgnoreCase))
return info;
bestMatch ??= info;
}
}
return bestMatch;
}
catch { }
return null;
}
private static PsnGameInfo? ParsePsnJson(string json)
{
try
{
using var doc = System.Text.Json.JsonDocument.Parse(json);
var root = doc.RootElement;
var info = new PsnGameInfo();
if (root.TryGetProperty("name", out var name))
info.Title = name.GetString() ?? "";
else if (root.TryGetProperty("title_name", out var tn))
info.Title = tn.GetString() ?? "";
if (root.TryGetProperty("provider_name", out var pub))
info.Publisher = pub.GetString() ?? "";
else if (root.TryGetProperty("publisher_name", out var pub2))
info.Publisher = pub2.GetString() ?? "";
if (root.TryGetProperty("release_date", out var rd))
{
string rawDate = rd.GetString() ?? "";
if (DateTime.TryParse(rawDate, out DateTime dt))
info.ReleaseDate = dt.ToString("yyyy-MM-dd");
else
info.ReleaseDate = rawDate;
}
if (root.TryGetProperty("long_desc", out var ld))
info.Description = StripHtml(ld.GetString() ?? "");
else if (root.TryGetProperty("short_desc", out var sd))
info.Description = StripHtml(sd.GetString() ?? "");
if (!string.IsNullOrWhiteSpace(info.Title))
return info;
}
catch { }
return null;
}
// Extract alternate title IDs from param.json serviceIdForSharing field.
// The payload strips newlines from param.json but keeps structure intact.
// Examples of entries to match:
// "UP4572-PPSA01768_00"
// "UP4572-CUSA12650_00"
// "UP4572-PPSA16388_00-00000000000HWOAD" (full contentId with suffix)
// Returns list of IDs in "PPSAxxxxx_00" or "CUSAxxxxx_00" format ready for API use.
private static List<string> ExtractAlternateTitleIds(string paramJson)
{
var ids = new List<string>();
if (string.IsNullOrWhiteSpace(paramJson)) return ids;
// Match publisher-prefix-free IDs: "PPSA12345_00" or "CUSA12345_00"
// Handles both "UP4572-PPSA01768_00" (short) and "UP4572-PPSA16388_00-SUFFIX" (full contentId)
var regex = new System.Text.RegularExpressions.Regex(@"(?:[A-Z]{2}\d{4}-)?(PPSA\d{5}_\d{2}|CUSA\d{5}_\d{2})");
var matches = regex.Matches(paramJson);
foreach (System.Text.RegularExpressions.Match m in matches)
{
if (m.Groups.Count >= 2)
{
string id = m.Groups[1].Value;
if (!ids.Contains(id)) ids.Add(id);
}
}
return ids;
}
// Strip HTML tags and decode common entities
private static string StripHtml(string html)
{
if (string.IsNullOrWhiteSpace(html)) return "";
// Replace <br>, <br/>, </p> with newlines
html = System.Text.RegularExpressions.Regex.Replace(html, @"<br\s*/?>", "\n", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
html = System.Text.RegularExpressions.Regex.Replace(html, @"</p>", "\n\n", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
// Strip all remaining tags
html = System.Text.RegularExpressions.Regex.Replace(html, @"<[^>]+>", "");
// Decode common HTML entities
html = System.Net.WebUtility.HtmlDecode(html);
// Collapse excessive whitespace but keep newlines
html = System.Text.RegularExpressions.Regex.Replace(html, @"[ \t]+", " ");
html = System.Text.RegularExpressions.Regex.Replace(html, @"\n{3,}", "\n\n");
return html.Trim();
}
private static string FormatSize(ulong bytes)
{
double mb = bytes / (1024.0 * 1024.0);
if (mb < 1024) return $"{mb:F1} MB ({bytes:N0} bytes)";
return $"{mb / 1024.0:F2} GB ({bytes:N0} bytes)";
}
private static string FormatJson(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return "(empty)";
// Basic pretty-print: add newlines after { [ , and before } ]
var sb = new System.Text.StringBuilder();
int depth = 0;
bool inString = false;
foreach (char c in raw)
{
if (c == '"') inString = !inString;
if (!inString)
{
if (c == '{' || c == '[')
{
sb.Append(c);
sb.Append('\n');
depth++;
sb.Append(new string(' ', depth * 2));
continue;
}
if (c == '}' || c == ']')
{
sb.Append('\n');
depth = System.Math.Max(0, depth - 1);
sb.Append(new string(' ', depth * 2));
sb.Append(c);
continue;
}
if (c == ',')
{
sb.Append(c);
sb.Append('\n');
sb.Append(new string(' ', depth * 2));
continue;
}
}
sb.Append(c);
}
return sb.ToString();
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+30
View File
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>PS5Upload</AssemblyName>
<RootNamespace>PS5Upload</RootNamespace>
<Version>1.0.0</Version>
<Authors>Manos</Authors>
<Description>High-Speed File Upload Tool for PS5</Description>
<Copyright>Copyright © 2026 Manos</Copyright>
<Nullable>enable</Nullable>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<ApplicationIcon>ps5_logo_fixed.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Resource Include="ps5_logo.png" />
<None Include="ps5_logo.ico">
<Link>ps5_logo.ico</Link>
</None>
</ItemGroup>
</Project>
+2238
View File
File diff suppressed because it is too large. Load diff
+29
View File
@@ -0,0 +1,29 @@
@echo off
echo ================================================
echo PS5 Upload Client - Building
echo By Manos
echo ================================================
echo.
echo [+] Building Windows GUI Client...
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true
if exist "bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe" (
echo.
echo ================================================
echo [+] SUCCESS! Built PS5Upload.exe
echo ================================================
echo.
echo Location: bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe
echo.
echo Copy PS5Upload.exe to any location and run it!
) else (
echo.
echo ================================================
echo [-] BUILD FAILED
echo ================================================
echo.
echo Make sure .NET 6.0 SDK is installed:
echo https://dotnet.microsoft.com/download/dotnet/6.0
exit /b 1
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+14
View File
@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:PS5UploadMobile"
x:Class="PS5UploadMobile.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+11
View File
@@ -0,0 +1,11 @@
namespace PS5UploadMobile;
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="PS5UploadMobile.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:PS5UploadMobile"
Shell.FlyoutBehavior="Disabled">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>
+9
View File
@@ -0,0 +1,9 @@
namespace PS5UploadMobile;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
+98
View File
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PS5UploadMobile.MainPage"
Title="PS5 Upload Suite">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto,Auto">
<!-- Top Bar with Menu -->
<Grid Grid.Row="0" Padding="10" BackgroundColor="#1E1E1E" ColumnDefinitions="Auto,*,Auto">
<Button Grid.Column="0" Text="☰" FontSize="24" BackgroundColor="Transparent"
Clicked="OnMenuClicked" WidthRequest="50"/>
<Label Grid.Column="1" Text="PS5 Upload Suite" FontSize="20" FontAttributes="Bold"
TextColor="White" VerticalOptions="Center" HorizontalOptions="Center"/>
<Button Grid.Column="2" Text="⭐" FontSize="20" BackgroundColor="Transparent"
Clicked="OnAddFavoriteClicked" WidthRequest="50"/>
</Grid>
<!-- Connection Panel -->
<Frame Grid.Row="1" Padding="10" Margin="10,5,10,5" BackgroundColor="#1E1E1E" CornerRadius="8">
<VerticalStackLayout Spacing="10">
<Label Text="PS5 Connection" FontSize="18" FontAttributes="Bold" TextColor="White"/>
<Grid ColumnDefinitions="*,*" ColumnSpacing="10">
<Entry x:Name="IpEntry" Grid.Column="0" Placeholder="PS5 IP (192.168.0.160)"
Text="192.168.0.160" TextColor="White" PlaceholderColor="Gray"/>
<Entry x:Name="PortEntry" Grid.Column="1" Placeholder="Port (9113)"
Text="9113" Keyboard="Numeric" TextColor="White" PlaceholderColor="Gray"/>
</Grid>
<Grid ColumnDefinitions="*,*" ColumnSpacing="10">
<Button x:Name="ConnectBtn" Grid.Column="0" Text="Connect"
Clicked="OnConnectClicked" BackgroundColor="#0078D4"/>
<Button x:Name="DisconnectBtn" Grid.Column="1" Text="Disconnect"
Clicked="OnDisconnectClicked" BackgroundColor="#D13438" IsEnabled="False"/>
</Grid>
<Label x:Name="StatusLabel" Text="Not connected" FontSize="12"
TextColor="Orange" HorizontalOptions="Center"/>
</VerticalStackLayout>
</Frame>
<!-- Path Navigation -->
<Frame Grid.Row="2" Padding="10" Margin="10,0,10,5" BackgroundColor="#2D2D2D" CornerRadius="8">
<Grid ColumnDefinitions="Auto,*,Auto">
<Button Grid.Column="0" Text="⬆️" Clicked="OnUpDirectoryClicked"
WidthRequest="50" BackgroundColor="#3E3E3E"/>
<Entry x:Name="PathEntry" Grid.Column="1" Text="/data/"
TextColor="White" Margin="10,0"/>
<Button Grid.Column="2" Text="🔄" Clicked="OnRefreshClicked"
WidthRequest="50" BackgroundColor="#3E3E3E"/>
</Grid>
</Frame>
<!-- File List -->
<Frame Grid.Row="3" Padding="0" Margin="10,0,10,5" BackgroundColor="#2D2D2D" CornerRadius="8">
<CollectionView x:Name="FileListView" SelectionMode="Single"
SelectionChanged="OnFileSelected">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="15,10" ColumnDefinitions="Auto,*,Auto">
<Label Grid.Column="0" Text="{Binding Icon}" FontSize="20"
VerticalOptions="Center" Margin="0,0,10,0"/>
<VerticalStackLayout Grid.Column="1" VerticalOptions="Center">
<Label Text="{Binding Name}" TextColor="White" FontSize="16"/>
<Label Text="{Binding Size}" TextColor="Gray" FontSize="12"/>
</VerticalStackLayout>
<Label Grid.Column="2" Text="{Binding Type}" TextColor="Gray"
FontSize="12" VerticalOptions="Center"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Frame>
<!-- Progress Bar -->
<Frame Grid.Row="4" Padding="10" Margin="10,0,10,5" BackgroundColor="#2D2D2D"
CornerRadius="8" IsVisible="{Binding IsTransferring}">
<VerticalStackLayout Spacing="5">
<Label x:Name="ProgressLabel" Text="Uploading..." TextColor="White" FontSize="14"/>
<ProgressBar x:Name="ProgressBar" Progress="0" ProgressColor="#0078D4"/>
<Label x:Name="ProgressPercentLabel" Text="0%" TextColor="Gray"
FontSize="12" HorizontalOptions="Center"/>
</VerticalStackLayout>
</Frame>
<!-- Action Buttons -->
<Grid Grid.Row="5" Padding="10" ColumnDefinitions="*,*,*" ColumnSpacing="10">
<Button Grid.Column="0" Text="📤 Upload" Clicked="OnUploadClicked"
BackgroundColor="#107C10"/>
<Button Grid.Column="1" Text="📥 Download" Clicked="OnDownloadClicked"
BackgroundColor="#0078D4"/>
<Button Grid.Column="2" Text="🗑️ Delete" Clicked="OnDeleteClicked"
BackgroundColor="#D13438"/>
</Grid>
</Grid>
</ContentPage>
+571
View File
@@ -0,0 +1,571 @@
#nullable enable
using PS5UploadMobile.Services;
using System.Collections.ObjectModel;
namespace PS5UploadMobile;
public partial class MainPage : ContentPage
{
private PS5UploadService _uploadService;
private ObservableCollection<FileItem> _files;
private string _currentPath = "/data/";
private FileItem? _selectedFile;
// New features
private List<string> _favorites = new List<string>();
private List<string> _debugLog = new List<string>();
private List<string> _transferHistory = new List<string>();
private const string FavoritesKey = "ps5_favorites";
// PS5 Profiles
private Dictionary<string, string> _ps5Profiles = new Dictionary<string, string>(); // name -> ip:port
private const string ProfilesKey = "ps5_profiles";
private string _currentProfileName = "";
public MainPage()
{
InitializeComponent();
_uploadService = new PS5UploadService();
_files = new ObservableCollection<FileItem>();
FileListView.ItemsSource = _files;
_uploadService.LogMessage += OnLogMessage;
_uploadService.ProgressChanged += OnProgressChanged;
LoadFavorites();
LoadProfiles();
}
// Menu button handler
private async void OnMenuClicked(object sender, EventArgs e)
{
string action = await DisplayActionSheet("Menu", "Cancel", null,
"🎮 PS5 Profiles",
"📋 Debug Log",
"📊 Transfer History",
"⭐ Favorites",
"🗑️ Clear Logs");
switch (action)
{
case "🎮 PS5 Profiles":
await ShowProfiles();
break;
case "📋 Debug Log":
await ShowDebugLog();
break;
case "📊 Transfer History":
await ShowTransferHistory();
break;
case "⭐ Favorites":
await ShowFavorites();
break;
case "🗑️ Clear Logs":
_debugLog.Clear();
_transferHistory.Clear();
await DisplayAlert("Cleared", "Logs cleared", "OK");
break;
}
}
private async Task ShowDebugLog()
{
string log = _debugLog.Count > 0
? string.Join("\n", _debugLog.TakeLast(50))
: "No log entries yet";
string action = await DisplayActionSheet("Debug Log", "Close", null, "📋 Copy Log", "👁️ View Log");
if (action == "📋 Copy Log")
{
await Clipboard.SetTextAsync(log);
await DisplayAlert("Copied", "Log copied to clipboard", "OK");
}
else if (action == "👁️ View Log")
{
await DisplayAlert("Debug Log", log, "OK");
}
}
private async Task ShowTransferHistory()
{
string history = _transferHistory.Count > 0
? string.Join("\n", _transferHistory.TakeLast(20))
: "No transfers yet";
await DisplayAlert("Transfer History", history, "OK");
}
private async Task ShowFavorites()
{
if (_favorites.Count == 0)
{
await DisplayAlert("Favorites", "No favorites saved.\nTap ⭐ to add current path.", "OK");
return;
}
// Add options for each favorite plus a delete option
var options = new List<string>();
foreach (var fav in _favorites)
{
options.Add($"📁 {fav}");
}
options.Add("🗑️ Delete a favorite");
string? selected = await DisplayActionSheet("Favorites", "Cancel", null, options.ToArray());
if (selected == null || selected == "Cancel")
return;
if (selected == "🗑️ Delete a favorite")
{
await DeleteFavorite();
}
else if (selected.StartsWith("📁 "))
{
string path = selected.Substring(3);
if (_favorites.Contains(path))
{
_currentPath = path;
PathEntry.Text = _currentPath;
await RefreshFileList();
}
}
}
private async Task DeleteFavorite()
{
if (_favorites.Count == 0) return;
string? toDelete = await DisplayActionSheet("Delete which favorite?", "Cancel", null, _favorites.ToArray());
if (toDelete != null && toDelete != "Cancel" && _favorites.Contains(toDelete))
{
_favorites.Remove(toDelete);
SaveFavorites();
await DisplayAlert("Deleted", $"'{toDelete}' removed from favorites", "OK");
}
}
private async void OnAddFavoriteClicked(object sender, EventArgs e)
{
if (_favorites.Contains(_currentPath))
{
bool remove = await DisplayAlert("Favorite Exists",
$"Remove '{_currentPath}' from favorites?", "Remove", "Cancel");
if (remove)
{
_favorites.Remove(_currentPath);
SaveFavorites();
await DisplayAlert("Removed", "Favorite removed", "OK");
}
}
else
{
_favorites.Add(_currentPath);
SaveFavorites();
await DisplayAlert("Added", $"'{_currentPath}' added to favorites", "OK");
}
}
private void LoadFavorites()
{
string? saved = Preferences.Get(FavoritesKey, "");
if (!string.IsNullOrEmpty(saved))
{
_favorites = saved.Split('|').Where(s => !string.IsNullOrEmpty(s)).ToList();
}
}
private void SaveFavorites()
{
Preferences.Set(FavoritesKey, string.Join("|", _favorites));
}
// PS5 Profile Management
private void LoadProfiles()
{
string? saved = Preferences.Get(ProfilesKey, "");
if (!string.IsNullOrEmpty(saved))
{
_ps5Profiles.Clear();
var entries = saved.Split('|').Where(s => !string.IsNullOrEmpty(s));
foreach (var entry in entries)
{
var parts = entry.Split('=');
if (parts.Length == 2)
{
_ps5Profiles[parts[0]] = parts[1];
}
}
}
}
private void SaveProfiles()
{
var entries = _ps5Profiles.Select(p => $"{p.Key}={p.Value}");
Preferences.Set(ProfilesKey, string.Join("|", entries));
}
private async Task ShowProfiles()
{
var options = new List<string>();
foreach (var profile in _ps5Profiles)
{
string marker = profile.Key == _currentProfileName ? "✓ " : "";
options.Add($"{marker}🎮 {profile.Key} ({profile.Value})");
}
options.Add("➕ Add new profile");
if (_ps5Profiles.Count > 0)
{
options.Add("🗑️ Delete a profile");
}
string? selected = await DisplayActionSheet("PS5 Profiles", "Cancel", null, options.ToArray());
if (selected == null || selected == "Cancel")
return;
if (selected == "➕ Add new profile")
{
await AddNewProfile();
}
else if (selected == "🗑️ Delete a profile")
{
await DeleteProfile();
}
else if (selected.Contains("🎮"))
{
// Extract profile name
int start = selected.IndexOf("🎮 ") + 3;
int end = selected.IndexOf(" (");
if (start > 2 && end > start)
{
string profileName = selected.Substring(start, end - start);
await SelectProfile(profileName);
}
}
}
private async Task AddNewProfile()
{
string? name = await DisplayPromptAsync("New Profile", "Enter profile name:", "Save", "Cancel", "My PS5");
if (string.IsNullOrEmpty(name)) return;
string? ip = await DisplayPromptAsync("PS5 IP", "Enter PS5 IP address:", "Save", "Cancel", "192.168.0.160");
if (string.IsNullOrEmpty(ip)) return;
string? port = await DisplayPromptAsync("Port", "Enter port (default 9113):", "Save", "Cancel", "9113", keyboard: Keyboard.Numeric);
if (string.IsNullOrEmpty(port)) port = "9113";
_ps5Profiles[name] = $"{ip}:{port}";
SaveProfiles();
await DisplayAlert("Saved", $"Profile '{name}' saved", "OK");
await SelectProfile(name);
}
private async Task DeleteProfile()
{
if (_ps5Profiles.Count == 0) return;
string? toDelete = await DisplayActionSheet("Delete which profile?", "Cancel", null, _ps5Profiles.Keys.ToArray());
if (toDelete != null && toDelete != "Cancel" && _ps5Profiles.ContainsKey(toDelete))
{
_ps5Profiles.Remove(toDelete);
SaveProfiles();
if (_currentProfileName == toDelete)
{
_currentProfileName = "";
}
await DisplayAlert("Deleted", $"Profile '{toDelete}' deleted", "OK");
}
}
private async Task SelectProfile(string profileName)
{
if (!_ps5Profiles.ContainsKey(profileName)) return;
string ipPort = _ps5Profiles[profileName];
var parts = ipPort.Split(':');
if (parts.Length == 2)
{
IpEntry.Text = parts[0];
PortEntry.Text = parts[1];
_currentProfileName = profileName;
AddLog($"Selected profile: {profileName} ({ipPort})");
await DisplayAlert("Profile Selected", $"Using '{profileName}'\nIP: {parts[0]}\nPort: {parts[1]}", "OK");
}
}
private async void OnConnectClicked(object sender, EventArgs e)
{
ConnectBtn.IsEnabled = false;
StatusLabel.Text = "Connecting...";
StatusLabel.TextColor = Colors.Yellow;
_uploadService.PS5Ip = IpEntry.Text;
_uploadService.Port = int.Parse(PortEntry.Text);
AddLog($"Connecting to {IpEntry.Text}:{PortEntry.Text}...");
bool connected = await _uploadService.ConnectAsync();
if (connected)
{
StatusLabel.Text = "Connected ✓";
StatusLabel.TextColor = Colors.Green;
ConnectBtn.IsEnabled = false;
DisconnectBtn.IsEnabled = true;
AddLog("Connected successfully");
await RefreshFileList();
}
else
{
StatusLabel.Text = "Connection failed";
StatusLabel.TextColor = Colors.Red;
ConnectBtn.IsEnabled = true;
AddLog("Connection failed");
}
}
private async void OnDisconnectClicked(object sender, EventArgs e)
{
await _uploadService.DisconnectAsync();
StatusLabel.Text = "Disconnected";
StatusLabel.TextColor = Colors.Orange;
ConnectBtn.IsEnabled = true;
DisconnectBtn.IsEnabled = false;
_files.Clear();
AddLog("Disconnected");
}
private async void OnRefreshClicked(object sender, EventArgs e)
{
await RefreshFileList();
}
private async void OnUpDirectoryClicked(object sender, EventArgs e)
{
if (_currentPath != "/")
{
int lastSlash = _currentPath.TrimEnd('/').LastIndexOf('/');
_currentPath = _currentPath.Substring(0, lastSlash + 1);
PathEntry.Text = _currentPath;
await RefreshFileList();
}
}
private async void OnFileSelected(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection.Count > 0)
{
_selectedFile = e.CurrentSelection[0] as FileItem;
if (_selectedFile?.IsDirectory == true)
{
_currentPath = _selectedFile.FullPath;
PathEntry.Text = _currentPath;
await RefreshFileList();
FileListView.SelectedItem = null;
}
}
}
private async void OnUploadClicked(object sender, EventArgs e)
{
try
{
var result = await FilePicker.PickAsync();
if (result != null)
{
ProgressLabel.Text = $"Uploading {result.FileName}...";
ProgressBar.Progress = 0;
ProgressPercentLabel.Text = "0%";
// Copy file to temp location for reliable access on Android
string tempPath = Path.Combine(FileSystem.CacheDirectory, result.FileName);
using (var sourceStream = await result.OpenReadAsync())
using (var destStream = File.Create(tempPath))
{
await sourceStream.CopyToAsync(destStream);
}
string remotePath = _currentPath.TrimEnd('/') + "/" + result.FileName;
AddLog($"Uploading: {result.FileName} -> {remotePath}");
bool success = await _uploadService.UploadFileAsync(tempPath, remotePath);
// Clean up temp file
try { File.Delete(tempPath); } catch { }
if (success)
{
AddTransfer($"⬆️ {result.FileName}");
await DisplayAlert("Success", "File uploaded successfully!", "OK");
await RefreshFileList();
}
else
{
await DisplayAlert("Error", "Upload failed", "OK");
}
}
}
catch (Exception ex)
{
AddLog($"Upload error: {ex.Message}");
await DisplayAlert("Error", $"Upload failed: {ex.Message}", "OK");
}
}
private async void OnDownloadClicked(object sender, EventArgs e)
{
if (_selectedFile == null || _selectedFile.IsDirectory)
{
await DisplayAlert("Error", "Please select a file to download", "OK");
return;
}
try
{
// First download to cache, then let user save via Share
string cachePath = Path.Combine(FileSystem.CacheDirectory, _selectedFile.Name);
ProgressLabel.Text = $"Downloading {_selectedFile.Name}...";
ProgressBar.Progress = 0;
ProgressPercentLabel.Text = "0%";
AddLog($"Downloading: {_selectedFile.FullPath} -> {cachePath}");
bool success = await _uploadService.DownloadFileAsync(_selectedFile.FullPath, cachePath);
if (success)
{
AddTransfer($"⬇️ {_selectedFile.Name}");
// Use Share to let user choose where to save
// This opens the Android share sheet where user can:
// - Save to Files app (choose location)
// - Save to Downloads
// - Send via other apps
string? action = await DisplayActionSheet(
$"'{_selectedFile.Name}' downloaded!",
"Close", null,
"💾 Save to location...",
"📂 Open file");
if (action == "💾 Save to location...")
{
// Share opens Android's save/share dialog where user can choose location
await Share.RequestAsync(new ShareFileRequest
{
Title = $"Save {_selectedFile.Name}",
File = new ShareFile(cachePath)
});
}
else if (action == "📂 Open file")
{
try
{
await Launcher.OpenAsync(new OpenFileRequest
{
File = new ReadOnlyFile(cachePath)
});
}
catch
{
await DisplayAlert("Error", "No app found to open this file type", "OK");
}
}
}
else
{
await DisplayAlert("Error", "Download failed", "OK");
}
}
catch (Exception ex)
{
AddLog($"Download error: {ex.Message}");
await DisplayAlert("Error", $"Download failed: {ex.Message}", "OK");
}
}
private async void OnDeleteClicked(object sender, EventArgs e)
{
if (_selectedFile == null)
{
await DisplayAlert("Error", "Please select a file to delete", "OK");
return;
}
bool confirm = await DisplayAlert("Confirm",
$"Delete {_selectedFile.Name}?", "Yes", "No");
if (confirm)
{
AddLog($"Deleting: {_selectedFile.FullPath}");
bool success = await _uploadService.DeleteFileAsync(_selectedFile.FullPath);
if (success)
{
AddTransfer($"🗑️ {_selectedFile.Name}");
await RefreshFileList();
}
}
}
private async Task RefreshFileList()
{
try
{
_files.Clear();
AddLog($"Listing: {_currentPath}");
var fileItems = await _uploadService.ListDirectoryAsync(_currentPath);
foreach (var fileItem in fileItems)
{
_files.Add(fileItem);
}
AddLog($"Found {fileItems.Count} items");
}
catch (Exception ex)
{
AddLog($"List error: {ex.Message}");
await DisplayAlert("Error", $"Failed to list directory: {ex.Message}", "OK");
}
}
private void AddLog(string message)
{
string timestamp = DateTime.Now.ToString("HH:mm:ss");
_debugLog.Add($"[{timestamp}] {message}");
if (_debugLog.Count > 100) _debugLog.RemoveAt(0);
}
private void AddTransfer(string message)
{
string timestamp = DateTime.Now.ToString("HH:mm:ss");
_transferHistory.Add($"[{timestamp}] {message}");
if (_transferHistory.Count > 50) _transferHistory.RemoveAt(0);
}
private void OnLogMessage(object? sender, string message)
{
MainThread.BeginInvokeOnMainThread(() =>
{
AddLog(message);
});
}
private void OnProgressChanged(object? sender, int progress)
{
MainThread.BeginInvokeOnMainThread(() =>
{
ProgressBar.Progress = progress / 100.0;
ProgressPercentLabel.Text = $"{progress}%";
});
}
}
+18
View File
@@ -0,0 +1,18 @@
namespace PS5UploadMobile;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
return builder.Build();
}
}
+47
View File
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-android</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>PS5UploadMobile</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Display name -->
<ApplicationTitle>PS5 Upload Suite</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.ps5tools.uploadsuite</ApplicationId>
<ApplicationIdGuid>0cb2ca56-8fa9-400b-8669-c219279a7aad</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<!-- Android specific -->
<AndroidManifest>Platforms\Android\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-android'))">
<PackageReference Include="System.Net.Sockets" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<!-- App Icon - PS5 Logo from Windows client -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Project>
+270
View File
@@ -0,0 +1,270 @@
#nullable enable
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace PS5UploadMobile.Services
{
public class FtpService
{
private string _ps5Ip = "192.168.0.160";
private int _ftpPort = 2121;
private TcpClient? _controlClient;
private NetworkStream? _controlStream;
private StreamReader? _controlReader;
public event EventHandler<string>? LogMessage;
public event EventHandler<int>? ProgressChanged;
public string PS5Ip
{
get => _ps5Ip;
set => _ps5Ip = value;
}
public int FtpPort
{
get => _ftpPort;
set => _ftpPort = value;
}
public async Task<bool> ConnectAsync()
{
try
{
_controlClient = new TcpClient();
await _controlClient.ConnectAsync(_ps5Ip, _ftpPort);
_controlStream = _controlClient.GetStream();
_controlReader = new StreamReader(_controlStream, Encoding.ASCII);
// Read welcome message
string? response = await _controlReader.ReadLineAsync();
LogMessage?.Invoke(this, $"Connected: {response}");
// Login as anonymous
await SendCommandAsync("USER anonymous");
await SendCommandAsync("PASS anonymous");
await SendCommandAsync("TYPE I"); // Binary mode
return true;
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Connection failed: {ex.Message}");
return false;
}
}
public async Task DisconnectAsync()
{
try
{
if (_controlClient?.Connected == true)
{
await SendCommandAsync("QUIT");
}
}
catch { }
finally
{
_controlReader?.Dispose();
_controlStream?.Dispose();
_controlClient?.Dispose();
}
}
public async Task<List<string>> ListDirectoryAsync(string path)
{
var files = new List<string>();
try
{
// Change to directory
await SendCommandAsync($"CWD {path}");
// Enter passive mode
string pasvResponse = await SendCommandAsync("PASV");
var dataEndpoint = ParsePasvResponse(pasvResponse);
// Connect to data channel
using var dataClient = new TcpClient();
await dataClient.ConnectAsync(dataEndpoint.Address, dataEndpoint.Port);
using var dataStream = dataClient.GetStream();
using var dataReader = new StreamReader(dataStream, Encoding.ASCII);
// Send LIST command
await SendCommandAsync("LIST");
// Read directory listing
string? line;
while ((line = await dataReader.ReadLineAsync()) != null)
{
files.Add(line);
}
dataClient.Close();
await ReadResponseAsync(); // Read final response
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"List failed: {ex.Message}");
}
return files;
}
public async Task<bool> UploadFileAsync(string localPath, string remotePath)
{
try
{
var fileInfo = new FileInfo(localPath);
long totalBytes = fileInfo.Length;
long uploadedBytes = 0;
// Enter passive mode
string pasvResponse = await SendCommandAsync("PASV");
var dataEndpoint = ParsePasvResponse(pasvResponse);
// Connect to data channel
using var dataClient = new TcpClient();
await dataClient.ConnectAsync(dataEndpoint.Address, dataEndpoint.Port);
using var dataStream = dataClient.GetStream();
// Send STOR command
await SendCommandAsync($"STOR {remotePath}");
// Upload file
using var fileStream = File.OpenRead(localPath);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await dataStream.WriteAsync(buffer, 0, bytesRead);
uploadedBytes += bytesRead;
int progress = (int)((uploadedBytes * 100) / totalBytes);
ProgressChanged?.Invoke(this, progress);
}
dataClient.Close();
await ReadResponseAsync(); // Read final response
LogMessage?.Invoke(this, $"Upload complete: {Path.GetFileName(localPath)}");
return true;
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Upload failed: {ex.Message}");
return false;
}
}
public async Task<bool> DownloadFileAsync(string remotePath, string localPath)
{
try
{
// Get file size
string sizeResponse = await SendCommandAsync($"SIZE {remotePath}");
long totalBytes = ParseSizeResponse(sizeResponse);
long downloadedBytes = 0;
// Enter passive mode
string pasvResponse = await SendCommandAsync("PASV");
var dataEndpoint = ParsePasvResponse(pasvResponse);
// Connect to data channel
using var dataClient = new TcpClient();
await dataClient.ConnectAsync(dataEndpoint.Address, dataEndpoint.Port);
using var dataStream = dataClient.GetStream();
// Send RETR command
await SendCommandAsync($"RETR {remotePath}");
// Download file
using var fileStream = File.Create(localPath);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await dataStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead);
downloadedBytes += bytesRead;
if (totalBytes > 0)
{
int progress = (int)((downloadedBytes * 100) / totalBytes);
ProgressChanged?.Invoke(this, progress);
}
}
dataClient.Close();
await ReadResponseAsync(); // Read final response
LogMessage?.Invoke(this, $"Download complete: {Path.GetFileName(remotePath)}");
return true;
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Download failed: {ex.Message}");
return false;
}
}
public async Task<bool> DeleteFileAsync(string remotePath)
{
try
{
await SendCommandAsync($"DELE {remotePath}");
LogMessage?.Invoke(this, $"Deleted: {remotePath}");
return true;
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Delete failed: {ex.Message}");
return false;
}
}
private async Task<string> SendCommandAsync(string command)
{
if (_controlStream == null || _controlReader == null)
throw new InvalidOperationException("Not connected");
byte[] commandBytes = Encoding.ASCII.GetBytes(command + "\r\n");
await _controlStream.WriteAsync(commandBytes, 0, commandBytes.Length);
return await ReadResponseAsync();
}
private async Task<string> ReadResponseAsync()
{
if (_controlReader == null)
throw new InvalidOperationException("Not connected");
string? response = await _controlReader.ReadLineAsync();
LogMessage?.Invoke(this, $"FTP: {response}");
return response ?? "";
}
private IPEndPoint ParsePasvResponse(string response)
{
// Parse PASV response: 227 Entering Passive Mode (192,168,0,160,8,19)
int start = response.IndexOf('(');
int end = response.IndexOf(')');
string[] parts = response.Substring(start + 1, end - start - 1).Split(',');
string ip = $"{parts[0]}.{parts[1]}.{parts[2]}.{parts[3]}";
int port = int.Parse(parts[4]) * 256 + int.Parse(parts[5]);
return new IPEndPoint(IPAddress.Parse(ip), port);
}
private long ParseSizeResponse(string response)
{
// Parse SIZE response: 213 12345
string[] parts = response.Split(' ');
return parts.Length > 1 ? long.Parse(parts[1]) : 0;
}
}
}
+241
View File
@@ -0,0 +1,241 @@
#nullable enable
using PS5Upload;
namespace PS5UploadMobile.Services
{
public class PS5UploadService
{
private string _ps5Ip = "192.168.0.160";
private int _port = 9113;
private PS5Protocol? _protocol;
public event EventHandler<string>? LogMessage;
public event EventHandler<int>? ProgressChanged;
public PS5UploadService()
{
_protocol = new PS5Protocol();
}
public string PS5Ip
{
get => _ps5Ip;
set => _ps5Ip = value;
}
public int Port
{
get => _port;
set => _port = value;
}
public bool IsConnected => _protocol?.IsConnected ?? false;
public async Task<bool> ConnectAsync()
{
try
{
_protocol = new PS5Protocol();
bool connected = await _protocol.ConnectAsync(_ps5Ip, _port);
if (connected)
{
LogMessage?.Invoke(this, $"Connected to PS5 at {_ps5Ip}:{_port}");
return true;
}
else
{
LogMessage?.Invoke(this, "Failed to connect to PS5");
return false;
}
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Connection error: {ex.Message}");
return false;
}
}
public Task DisconnectAsync()
{
if (_protocol != null)
{
_protocol.Disconnect();
LogMessage?.Invoke(this, "Disconnected from PS5");
}
return Task.CompletedTask;
}
public async Task<List<FileItem>> ListDirectoryAsync(string path)
{
var files = new List<FileItem>();
if (_protocol == null || !_protocol.IsConnected)
{
LogMessage?.Invoke(this, "Not connected to PS5");
return files;
}
try
{
var entries = await _protocol.ListDirAsync(path);
foreach (var entry in entries)
{
string fullPath = path.TrimEnd('/') + "/" + entry.Name;
if (entry.IsDirectory) fullPath += "/";
files.Add(new FileItem
{
Name = entry.Name,
Size = entry.IsDirectory ? "" : FormatFileSize(entry.Size),
Type = entry.IsDirectory ? "DIR" : "FILE",
Icon = entry.IsDirectory ? "📁" : "📄",
IsDirectory = entry.IsDirectory,
FullPath = fullPath
});
}
LogMessage?.Invoke(this, $"Listed {files.Count} items in {path}");
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"List error: {ex.Message}");
}
return files;
}
public async Task<bool> UploadFileAsync(string localPath, string remotePath)
{
if (_protocol == null || !_protocol.IsConnected)
{
LogMessage?.Invoke(this, "Not connected to PS5");
return false;
}
try
{
var fileInfo = new FileInfo(localPath);
LogMessage?.Invoke(this, $"Uploading {fileInfo.Name} ({FormatFileSize(fileInfo.Length)})...");
var progress = new Progress<UploadProgress>(p =>
{
int percent = p.TotalBytes > 0 ? (int)((p.BytesSent * 100) / p.TotalBytes) : 0;
ProgressChanged?.Invoke(this, percent);
});
bool success = await _protocol.UploadFileAsync(localPath, remotePath, progress);
if (success)
{
LogMessage?.Invoke(this, $"Upload complete: {fileInfo.Name}");
return true;
}
else
{
LogMessage?.Invoke(this, "Upload failed");
return false;
}
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Upload error: {ex.Message}");
return false;
}
}
public async Task<bool> DownloadFileAsync(string remotePath, string localPath)
{
if (_protocol == null || !_protocol.IsConnected)
{
LogMessage?.Invoke(this, "Not connected to PS5");
return false;
}
try
{
LogMessage?.Invoke(this, $"Downloading {Path.GetFileName(remotePath)}...");
var progress = new Progress<UploadProgress>(p =>
{
int percent = p.TotalBytes > 0 ? (int)((p.BytesSent * 100) / p.TotalBytes) : 0;
ProgressChanged?.Invoke(this, percent);
});
bool success = await _protocol.DownloadFileAsync(remotePath, localPath, progress);
if (success)
{
LogMessage?.Invoke(this, $"Download complete: {Path.GetFileName(remotePath)}");
return true;
}
else
{
LogMessage?.Invoke(this, "Download failed");
return false;
}
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Download error: {ex.Message}");
return false;
}
}
public async Task<bool> DeleteFileAsync(string remotePath)
{
if (_protocol == null || !_protocol.IsConnected)
{
LogMessage?.Invoke(this, "Not connected to PS5");
return false;
}
try
{
bool success = await _protocol.DeleteFileAsync(remotePath);
if (success)
{
LogMessage?.Invoke(this, $"Deleted: {remotePath}");
return true;
}
else
{
LogMessage?.Invoke(this, "Delete failed");
return false;
}
}
catch (Exception ex)
{
LogMessage?.Invoke(this, $"Delete error: {ex.Message}");
return false;
}
}
private string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len /= 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
public class FileItem
{
public string Name { get; set; } = "";
public string Size { get; set; } = "";
public string Type { get; set; } = "";
public string Icon { get; set; } = "";
public bool IsDirectory { get; set; }
public string FullPath { get; set; } = "";
}
}
File diff suppressed because it is too large. Load diff
+23
View File
@@ -0,0 +1,23 @@
# PS5 Upload Server - Makefile
# Uses the maintained ps5-payload-dev/sdk (2026 release)
PS5_HOST ?= 192.168.0.160
PS5_PORT ?= 9021
PS5_PAYLOAD_SDK := /opt/ps5-payload-sdk
CC := $(PS5_PAYLOAD_SDK)/bin/prospero-clang
CXX := $(PS5_PAYLOAD_SDK)/bin/prospero-clang++
LD := $(PS5_PAYLOAD_SDK)/bin/prospero-lld
ELF := ps5_upload_server.elf
CFLAGS := -Wall -O3
all: $(ELF)
$(ELF): main.c
$(CC) $(CFLAGS) -o $@ $^ -lkernel_sys -lSceSystemService -lSceUserService -lSceAppInstUtil
clean:
rm -f $(ELF)
.PHONY: all clean
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
echo "================================================"
echo "PS5 Upload Server - Compilation"
echo "By Manos"
echo "================================================"
echo ""
echo "[+] Compiling..."
rm -f ps5_upload_server.elf
/opt/ps5-payload-sdk/bin/prospero-clang -Wall -O3 -pthread \
-I/opt/ps5-payload-sdk/target/include_bsd \
-I/opt/ps5-payload-sdk/target/include \
-L/opt/ps5-payload-sdk/target/lib \
-o ps5_upload_server.elf main.c
if [ -f "ps5_upload_server.elf" ]; then
echo ""
echo "================================================"
echo "[+] SUCCESS! Compiled ps5_upload_server.elf"
echo "================================================"
echo ""
ls -lh ps5_upload_server.elf
file ps5_upload_server.elf
echo ""
echo "Upload to PS5 and run with elfldr"
echo "Server will listen on port 9113"
else
echo ""
echo "================================================"
echo "[-] COMPILATION FAILED"
echo "================================================"
exit 1
fi
+4402
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+343
View File
@@ -0,0 +1,343 @@
# 🚀 PS5 Upload Suite - High-Speed File Transfer
**By Manos**
**Version 4.1.0 - Memory Optimization & Large File Handling**
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **104+ MB/s** upload speeds for large files using parallel chunked uploads!
⭐ **NEW in v4.1.0:**
- 🧠 **Memory Optimization** - Handles 156K+ files without crashes
- ⚡ **Reduced UI Stuttering** - Batch logging for smoother experience
- 🔧 **Lock-Free Operations** - ConcurrentDictionary for thread-safety
- 🧹 **Auto Memory Cleanup** - Completed files removed from tracking
- � **Incremental Processing** - No massive dictionary allocations
📱 **Android Mobile Client available!**
![Transfer History & Multi-PS5 Support](screenshots/screenshot3.png)
![Mobile App](screenshots/mobile_screenshot.jpg)
---
## 📦 Downloads
Download the latest release from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page:
### 1. PS5 Server Payload
- **File:** `ps5_upload_server.elf`
- **Port:** 9113
- High-speed custom binary protocol
- 16MB socket buffers for maximum throughput
- Multi-threaded client handling
### 2. Windows GUI Client
- **File:** `PS5Upload.exe`
- Self-contained executable (no .NET installation required)
- Modern dark theme UI
- Drag & drop file/folder upload
- Real-time upload progress with speed tracking
- Transfer History with success/failed tracking
- Privacy-focused (no storage monitoring)
### 3. 📱 Android Mobile Client
- **File:** `PS5UploadMobile.apk`
- Upload/download files from your phone
- Multi-PS5 Profiles support
- Browse PS5 filesystem
- Favorites for quick navigation
---
## 🚀 Quick Start
### Step 1: Load PS5 Payload
1. Download `ps5_upload_server.elf` from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page
2. Copy it to your PS5: `/data/etaHEN/payloads/ps5_upload_server.elf`
3. Load the payload with elfldr
4. You should see notification: `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos`
### Step 2: Run Windows Client
1. Run `PS5Upload.exe` on your PC
2. Enter your PS5's IP address (e.g., `192.168.0.160`)
3. Click **Connect**
4. Browse files/folders or drag & drop
5. Click **Upload to PS5**
---
## 🎨 Features
### PS5 Server
✅ **High-Speed Protocol** - Custom binary protocol
✅ **4MB Buffers** - Maximum throughput
✅ **Direct Disk I/O** - No temp files
✅ **Multi-threaded** - Handle multiple clients
✅ **Robust Error Handling** - Graceful failures
### Windows Client
✅ **Modern UI** - Dark theme, maximized window for better visibility
✅ **Drag & Drop** - Files and folders
✅ **Browse PS5** - Navigate filesystem
✅ **Real-time Progress** - Speed & percentage tracking
✅ **Optimized Upload** - 8 parallel connections for 88-110 MB/s aggregate speed
✅ **Transfer History** - Track all uploads/downloads with success/failed status
✅ **Auto-Clear History** - Optional auto-clear on startup
✅ **Folder Upload** - Recursive directory upload
✅ **Download Files** - Download from PS5 to PC
✅ **🔍 Smart Search** - Full filesystem indexing with instant search (NEW v3.3)
- Wildcard support: `*.pkg`, `*loader*`, `game*.bin`
- Size filters: `size:>1GB`, `size:<100MB`
- Case-insensitive matching
- Search both filename and full path
- Double-click to navigate to folder
✅ **💻 Shell Terminal** - Execute commands directly on PS5 (NEW v3.3)
- Run system commands remotely
- Real-time output display
- Command history
- Working directory support
✅ **Favorites/Bookmarks** - Quick navigation to saved paths
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles
---
## 📊 Performance
### v3.1.0 Optimized Results:
- ✅ **104+ MB/s** upload speed for large files (parallel chunked uploads)
- ✅ **4 parallel chunk connections** for maximum throughput
- ✅ **500MB chunks** - optimal size for PS5 disk performance
- ✅ **Real-time progress** - Speed/ETA updates during upload
- ✅ **Zero chunk failures** - Fixed race conditions
- ✅ **Fully responsive UI** throughout upload
- ✅ **No memory leaks** - stable operation
### Previous Results (v1.3.0):
- ✅ **42,801 files** uploaded successfully
- ✅ **60-150 MB/s** sustained throughput
| Network | Expected Upload Speed | Expected Download Speed |
|---------|----------------------|------------------------|
| **Gigabit Ethernet** | 88-110 MB/s (aggregate) | 100-120 MB/s |
| **WiFi 6 (5GHz)** | 32-70 MB/s (aggregate) | 60-80 MB/s |
| **WiFi 5 (5GHz)** | 20-50 MB/s (aggregate) | 40-60 MB/s |
**Note:** Upload speeds depend on network quality and PS5 disk write performance. WiFi has ~40-60% overhead compared to Ethernet.
---
## 🛠️ Troubleshooting
### Connection Failed
- Make sure PS5 payload is running
- Check PS5 IP address is correct
- Verify both devices are on same network
- Check firewall settings
### Slow Upload Speed
- Use wired Ethernet connection (not WiFi)
- Close other network applications
- Check PS5 disk health
### Upload Fails
- Check PS5 has enough free space
- Verify destination path exists
- Check file permissions
---
## 🔒 Security Notes
- Server only accepts connections from local network
- No authentication required (local network only)
- SHUTDOWN command only works from localhost
---
## 📝 What's New in v3.2.0
### 🎯 Improvements:
#### 1. 💾 Accurate Storage Display
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
- **Accounts for reserved space** - Same calculation as PS5 system
#### 2. 📱 Mobile App Improvements
- **Multi-select mode** - Toggle button to select multiple files/folders
- **Folder browser** - Visual folder picker for Copy/Move destinations
- **Sorted file list** - Folders always displayed at top
- **Exit confirmation** - Prompt when pressing back button
- **Delete folders** - Now supports folder deletion (not just files)
- **Batch operations** - Delete/download multiple items at once
#### 3. 🐛 Bug Fixes
- **Fixed empty folder deletion** - No more "Unexpected response" error
- **Fixed Copy/Move crash** - No longer crashes when selecting destination
- **Fixed multi-select count** - Correct item count in delete confirmation
- **Fixed folder navigation** - Can enter folders after actions complete
---
## 📝 What's New in v3.1.0
### 🚀 Performance Improvements:
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
- **Large files (>100MB)** are now split into 500MB chunks
- **4 parallel connections** upload chunks simultaneously
- **Result:** ~104 MB/s upload speed (2x faster than before!)
- **Fixed race condition:** First chunk creates file before others start
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
#### 2. 📊 Real-time Progress Display
- **Speed display** now updates in real-time during chunked uploads
- **ETA display** shows accurate time remaining
- **Progress bar** correctly shows 0-100% for chunked files
- No more speed dropping to 0 during large file uploads
#### 3. 💾 Storage Display Improvements
- **Changed label** to "Data Storage (/data)" for clarity
- **Uses statfs with f_bavail** for more accurate available space
- **Real-time updates** every 5 seconds when connected
---
## 📝 What's New in v3.0.0
### 🐛 Critical Bug Fixes:
#### 1. 🔧 Path Normalization Fix
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
- **Solution:** Added `NormalizePath()` function in both client and server
- **Result:** 100% reliable directory creation and file uploads
#### 2. 📱 Android Mobile Client
- **NEW:** Full-featured Android app for PS5 file management
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
- Upload files from phone to PS5
- Download files from PS5 with Share option
- Browse PS5 filesystem
- Favorites for quick navigation
- Debug Log with Copy to clipboard
- Transfer History tracking
#### 3. 🛠️ Server Improvements
- Path normalization in all handler functions
- More robust error handling
- Improved stability for parallel uploads
---
## 📝 What's New in v2.1.0
### 🚀 Performance Optimizations:
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
- **Per-file:** 11-14 MB/s sustained per connection
#### 2. 📊 Transfer History
- Complete history of all uploads and downloads
- Success/Failed status tracking with error messages
- Speed statistics (average, min, max)
- Export to CSV/JSON for analysis
- Persistent storage across sessions
#### 3. 🔄 Auto-Clear History on Startup
- Optional checkbox to clear history automatically
- Useful for keeping UI clean between sessions
- Setting saved in `ps5_upload_settings.json`
#### 4. 🖥️ Maximized Window UI
- Application opens in full-screen mode by default
- Better visibility for large file transfers
- Can be resized/restored as needed
### Technical Improvements:
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
---
## 📝 What's New in v2.0.0
### 🎉 4 Major New Features:
#### 1. 📥 Download Files (PS5 → PC)
- Right-click any file → "⬇️ Download to PC"
- Save file dialog for destination selection
- Real-time progress tracking with speed display
- Optimized with sendfile for maximum speed
#### 2. 🔍 File Search
- Search box in PS5 Files panel
- Real-time filtering as you type
- Case-insensitive search
- Quick "Clear" button to reset
#### 3. ⭐ Favorites/Bookmarks
- Save frequently used PS5 paths
- Quick dropdown navigation
- Add/Remove favorite paths
- Persistent storage in JSON
#### 4. 🎮 Multi-PS5 Support
- Save multiple PS5 profiles (IP + name)
- Quick switch between different PS5 consoles
- Dropdown profile selector
- Persistent profile storage
### Previous Stability (v1.3.0):
- ✅ **Zero connection drops** - 5 minute socket timeout + aggressive keepalive
- ✅ **Fully responsive UI** - Async updates + throttled logging
- ✅ **No memory leaks** - Proper resource disposal
- ✅ **Tested:** 42,801 files uploaded with 100% success rate
---
## 📝 License
This software is provided as-is for personal use.
---
## 👤 Author
**Manos**
Created with ❤️ for the PS5 homebrew community
---
## 🙏 Credits
- PS5 SDK
- etaHEN
- Inspired by ps5upload by PhantomPtr
- PS5 homebrew community
---
## 📞 Support
For issues or questions, please open an issue on GitHub.
---
**Enjoy blazing-fast file transfers on your PS5!** 🚀
Binary file not shown.
Binary file not shown.
+142
View File
@@ -0,0 +1,142 @@
# 🚀 PS5 Upload Suite v5.0.0 — Complete Management Suite
Complete all-in-one PS5 management platform — **104+ MB/s** file transfers, live hardware monitoring, full game/save/screenshot management, and deep PS5 integration through a single desktop client + a robust PS5 payload.
---
## 📦 Downloads
| File | Size | Purpose |
|------|------|---------|
| `PS5Upload.exe` | 146 MB | Windows GUI client (self-contained, no .NET install required) |
| `ps5_upload_server.elf` | 187 KB | PS5 payload — load via elfldr, listens on port 9113 |
**Load the payload on your PS5:** copy `ps5_upload_server.elf` to `/data/etaHEN/payloads/` and run it. You will see a notification `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos` confirming it's ready.
---
## 🆕 What's New in v5.0.0
### 🖥️ NEW: Hardware Monitor Tab
- **System Info** — Model (CFI-xxxx), Serial Number, Architecture, OS version
- **Physical RAM detection** — 5-step fallback chain (PS5-specific API → sysctl chain → 16 GB default)
- **Live Sensors** — CPU temperature, SoC temperature, CPU frequency, SoC power consumption
- **Auto-refresh** — Configurable 5-second polling with automatic disconnect detection
- **Payload hardened** — `pthread_mutex_t` + 1s cache for sensors, 5s throttle for power API, rejection of abnormal readings
### 📷 NEW: Screenshots Tab
- **Visual Gallery** — Live thumbnail previews (200px downsampled for fast loading)
- **Smart Cache** — Local cache in `%TEMP%` keyed by remote path hash — zero re-downloads on refresh
- **Batch Download** — Multi-select and download multiple screenshots
- **Clean Delete** — Removes ALL 3 files per screenshot (`.dat`, `.meta`, `.jpg.jpeg`) — **no more ghost entries in PS5 Media Gallery**
- **Double-click Preview** — Opens in default image viewer
- **Full Path Scanning** — 5-level recursive scan of `/user/av_contents/thumbnails/photo/…`
### ▶️ NEW: Launch Game
- Right-click any mounted game → **Launch Game**
- Uses low-level `sceLncUtilLaunchApp` with fallback to `sceSystemServiceLaunchApp`
- **Works on pirated/sideloaded games** (bypasses `0x80940005` permission error)
- Triple-strategy launcher with detailed diagnostic error messages
### 💾 NEW: Save Manager Tab
- Per-game save browsing with metadata
- Game icons fetched and cached from PSN/param.sfo
- Batch download saves to local backup folder
- Upload saves back to PS5
- Size, date, and title ID display
### 🎮 NEW: Game Details Window
- **PSN Cover Art** fetched from PlayStation Store
- Full `param.json` metadata formatted nicely
- Online PSN info: description, genres, age rating, publisher
- Alternate title ID detection (regional variants)
- Browser search fallback if API fails
### 🔧 Improved Storage Reporting
- ✅ Matches **PS5 Settings "Console Storage"** formula exactly
- ✅ Total: `/user` effective + `/system_data` + `/system_ex`
- ✅ Free: proper `f_bavail` across all partitions
- ✅ Accounts for reserved space & per-partition blocksize
- Example real-world PS5: **848 GB total / 354 GB free** ✓
### 🎮 Improved Mount Games
- ✅ Correct `sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0)` signature
- ✅ Registration BEFORE `mount.lnk` (correct order)
- ✅ Direct in-payload registration — no external `game_mounter.elf` dependency
- ✅ Proper DRM patching & metadata copy
### 🛡️ Stability Overhaul
- ✅ **Thread-safe sensor reads** — `pthread_mutex_t` prevents concurrent API crashes
- ✅ **API call throttling** — SoC power read at most every 5s, sensor cache every 1s
- ✅ **Cached static HW info** — Read once, serve forever (Model, Serial, OS)
- ✅ **Unsafe APIs removed** — `sceKernelIccGet*`, `HwHasWlanBt`, `HwHasOpticalOut` (caused crashes)
- ✅ **Client busy-flag guard** — `Interlocked` prevents overlapping refreshes
- ✅ **Auto-stop timers** — Hardware auto-refresh stops on disconnect
- ✅ **Graceful reconnect** — Connection lost → automatic reconnection attempt
---
## 🐛 Bug Fixes
- **Launch failed with 0x80940005** — Fixed by using low-level `sceLncUtilLaunchApp` instead of `sceSystemServiceLaunchApp` for pirated/mounted games
- **Payload crashes on Hardware Tab refresh** — Fixed with mutex, caching, and removal of unsafe `sceKernelIccGet*` APIs
- **Connection freezes after auto-refresh enabled** — Fixed with busy-flag guard, auto-stop on disconnect, and longer polling interval
- **Ghost screenshots in PS5 Media Gallery after delete** — Fixed by deleting all 3 associated files (`.dat`, `.meta`, `.jpg.jpeg`)
- **Physical RAM showing "—"** — Fixed with 5-step detection chain including PS5-specific `sceKernelGetDirectMemorySize()`
- **Wi-Fi / Bluetooth showing "✗ None"** — Fixed (hardcoded to `true` since all PS5 models have it)
- **Screenshots showing 0 items** — Fixed by scanning correct path `/user/av_contents/thumbnails/photo/` (where viewable `.jpg.jpeg` files live)
- **Storage reporting mismatch with PS5 Settings** — Fixed with correct aggregation formula
---
## 🔧 Technical Details
### New Payload Commands
- `CMD_LAUNCH_GAME (0x44)` — Launch mounted games
- `CMD_LIST_SCREENSHOTS (0x45)` — Recursive scan of screenshot directories
- `CMD_DELETE_SCREENSHOT (0x46)` — Clean delete (original + thumbnail + metadata)
### New Sony Kernel APIs Used (verified safe)
- `sceKernelGetHwModelName` / `sceKernelGetHwSerialNumber`
- `sceKernelGetCpuTemperature` / `sceKernelGetSocSensorTemperature`
- `sceKernelGetCpuFrequency`
- `sceKernelGetDirectMemorySize`
- `sceKernelGetSocPowerConsumption` (throttled to 5s)
- `sceLncUtilLaunchApp` / `sceLncUtilInitialize`
- `sceUserServiceInitialize` / `sceUserServiceGetForegroundUser`
### Removed APIs (destabilize payload)
- `sceKernelIccGetPowerOperatingTime`
- `sceKernelIccGetPowerNumberOfBootShutdown`
- `sceKernelHwHasWlanBt` / `sceKernelHwHasOpticalOut`
- `sceShellCoreUtilGetEffectiveTotalSizeOfUserPartition` (privileges issue)
---
## 🚀 Quick Start
1. **Load payload on PS5:** Copy `ps5_upload_server.elf` to `/data/etaHEN/payloads/` and execute it
2. **Run client on PC:** Launch `PS5Upload.exe` on Windows
3. **Connect:** Enter your PS5 IP (e.g. `192.168.0.160`) and click Connect
4. **Enjoy:** 8 tabs of full PS5 management — uploads, games, saves, screenshots, live hardware monitoring, and more!
---
## 📋 Full Feature List
See the [README](https://github.com/manos555555/PS5-Upload-Suite/blob/main/README.md) for the complete feature matrix.
---
## 🙏 Credits
- PS5 SDK by John Törnblom
- etaHEN team for the kernel runtime
- PS5 homebrew community
- Inspired by `ps5upload` by PhantomPtr
---
**Tested on:** PS5 Slim (CFI-2016 A01Y), firmware with etaHEN — 16 GB RAM, 16 CPU cores, ~848 GB storage
🎮 **Enjoy the complete PS5 management experience!**
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

After

Width:  |  Height:  |  Size: 93 KiB