Compare commits

...
44 Commits
Author SHA1 Message Date
Manos 3526f4874b v5.0.0 mobile: full feature parity with desktop (Games/Saves/Screenshots/Hardware/Shell/MountGames tabs) + light flyout menu + 0 warnings 2026-04-19 13:34:35 +03:00
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
Manos 4403d86be5 Update README for v3.2.0 - Storage display fix, mobile improvements, bug fixes 2026-01-24 01:24:50 +02:00
Manos af252efc9a Clean up README - remove source code references and building instructions 2026-01-20 04:11:59 +02:00
Manos 2da27aed13 Remove old release files and zips 2026-01-20 04:05:11 +02:00
Manos 264ad6210d Remove source code - keep only compiled binaries in releases 2026-01-20 04:01:47 +02:00
Manos db48bc6fb2 Update README.md for v3.1.0 - parallel chunked uploads, real-time progress 2026-01-20 04:00:11 +02:00
Manos 4ac43ebb8b v3.1.0: Parallel chunked uploads (104 MB/s), real-time speed/ETA, storage display fixes 2026-01-20 03:54:44 +02:00
Manos 20caced5f4 Keep only screenshot3 and mobile screenshot in README 2026-01-19 17:24:25 +02:00
Manos 285279788f Add mobile app screenshot to README 2026-01-19 17:22:41 +02:00
Manos 70d174cecc v3.0.0 - Rename release to version 3.0.0 2026-01-19 17:14:53 +02:00
manos555555 f128c6a6b4 Delete .gitignore 2026-01-19 17:00:00 +02:00
Manos c211c086ac v2.2.0 - Critical path fix, Android Mobile App with Multi-PS5 Profiles 2026-01-19 16:58:51 +02:00
manos555555 4ca30876c7 Update README.md 2026-01-19 01:26:59 +02:00
manos555555 5ae3c1709e Delete PROTOCOL.md 2026-01-19 01:25:27 +02:00
Manos 3374e87bfe Update to v2.1.0: Add screenshot, update README, remove old release files 2026-01-19 01:23:51 +02:00
Manos c21d73f41d Resolved merge conflict - keeping v2.0.0 ZIP 2026-01-18 03:34:02 +02:00
Manos 5ce9d470bc v2.0.0 - Added Download Files, File Search, Favorites/Bookmarks, Multi-PS5 Support 2026-01-18 03:33:33 +02:00
manos555555 0eb159bf88 Delete PS5Upload-Windows-x64-v1.0.zip 2026-01-14 01:37:31 +02:00
manos555555 4ff160232f Delete PS5Upload-Windows-x64-v1.1-BugFix.zip 2026-01-14 01:37:21 +02:00
manos555555 a5e40ce245 Delete PS5Upload-Windows-x64-v1.2-Complete.zip 2026-01-14 01:37:13 +02:00
manos555555 bb7d90411c Delete PS5Upload-Windows-x64-v1.3.0-Stable.zip 2026-01-14 01:37:06 +02:00
manos555555 00013aae76 Delete GITHUB_UPLOAD_INSTRUCTIONS.md 2026-01-14 01:36:42 +02:00
manos555555 6d8d802962 Delete create_v1.1_release.ps1 2026-01-14 01:36:32 +02:00
manos555555 f3a1d5e436 Delete create_github_release.ps1 2026-01-14 01:36:22 +02:00
manos555555 bad3029e32 Delete create_release.bat 2026-01-14 01:36:13 +02:00
manos555555 a51fffef83 Delete create_v1.2_release.ps1 2026-01-14 01:35:43 +02:00
manos555555 f89eaf4e31 Delete upload_to_github.py 2026-01-14 01:35:22 +02:00
Manos f34fdf8e63 v1.4.1 - UI Fix: Increase Dialog Size for Better Visibility
🔧 Fix:
- Increased dialog height from 250 to 320 pixels
- Increased dialog width from 500 to 550 pixels
- All buttons (Replace, Skip, Replace All, Skip All) now fully visible

📝 Files Changed:
- client/DuplicateFileDialog.xaml: Updated Height and Width properties
2026-01-13 01:43:01 +02:00
Manos 8479221915 v1.4.0 - Feature: Duplicate File Detection with Replace/Skip Dialog
🎯 New Feature:
- Added duplicate file detection before upload
- Shows dialog when file already exists on PS5
- Options: Replace / Skip / Replace All / Skip All

📋 How It Works:
1. Before upload, checks all files against existing PS5 files
2. Groups files by directory to minimize ListDir calls
3. Shows dialog for each duplicate with file sizes
4. User can choose action per file or apply to all

🎨 UI:
- New DuplicateFileDialog.xaml with modern dark theme
- Shows local and remote file sizes
- 4 action buttons with clear labels

💡 Benefits:
- No need to manually delete folders before re-upload
- Skip already uploaded files automatically
- Replace only changed files
- Saves time on failed upload retries

📝 Files Changed:
- client/MainWindow.xaml.cs: Added FilterDuplicateFilesAsync method
- client/DuplicateFileDialog.xaml: New dialog UI
- client/DuplicateFileDialog.xaml.cs: Dialog logic with FileAction enum

🔧 Technical:
- Uses existing ListDir protocol to check file existence
- Efficient: Groups files by directory to minimize network calls
- Maintains user choice (Replace All / Skip All) across files
2026-01-13 01:41:19 +02:00
Manos e3bfa54994 v1.3.1 - Hotfix: Unlimited Socket Timeout for Large Files
🔧 Critical Fix:
- Changed socket timeout from 5 minutes to UNLIMITED (0)
- Supports files of ANY size (50GB+)
- Keepalive protection prevents zombie connections

📊 Why This Fix:
- 5 minute timeout was insufficient for large files with 6 parallel connections
- Example: 15GB file @ 30 MB/s = 8.3 minutes (exceeded 5 min timeout)
- With unlimited timeout + keepalive, files of any size will upload successfully

🛡️ Safety:
- Aggressive keepalive (10s idle, 5s probe, 3 retries) detects dead connections in ~25s
- No risk of zombie connections or resource leaks
- Dead connections are automatically closed by keepalive

📝 Files Changed:
- payload/main.c: Changed tv.tv_sec from 300 to 0
- payload/ps5_upload_server.elf: Recompiled with unlimited timeout
2026-01-13 01:33:29 +02:00
Manos 68d586de97 v1.3.0 - Stable Release: Complete Stability Overhaul
🎯 Major Achievement
- Fixed all 15 critical bugs causing connection drops, UI freezes, and upload failures
- Tested: 42,801+ files uploaded with ZERO errors and full gigabit speeds

🔧 Payload Fixes (7 bugs):
1. Per-file mutex implementation for concurrent uploads
2. Memory/mutex cleanup on disconnect
3. fflush() race condition fix (moved inside mutex)
4. File creation race condition fix (added mutex lock)
5. Global scan counter issues (moved to global scope)
6. Directory counting bug (count dirs + files)
7. Socket timeout for large files (5 min timeout + aggressive keepalive) ⭐ CRITICAL

🔧 Client Fixes (8 bugs):
1. ConnectAsync timeout memory leak
2. ConnectAsync exception memory leak
3. Disconnect memory leak
4. Upload deadlock during chunked uploads
5. Chunking disabled for stability (PS5 can't handle concurrent writes) ⭐ CRITICAL
6. UI freeze during uploads (Dispatcher.Invoke → InvokeAsync)
7. Log flooding causing UI freeze (throttling + size limit)
8. Optimal parallel connection strategy (6 parallel single-connection uploads)

📊 Performance Results:
- ✅ 42,801 files uploaded successfully
- ✅ Zero errors - 100% success rate
- ✅ 60-150 MB/s sustained throughput
- ✅ Peak: 190 MB/s per connection
- ✅ Fully responsive UI throughout upload
- ✅ No memory leaks - stable operation

📝 Files Changed:
- payload/main.c: 7 critical fixes
- payload/ps5_upload_server.elf: Recompiled with all fixes
- client/Protocol.cs: 3 memory leak fixes
- client/MainWindow.xaml.cs: 5 stability fixes
- CHANGELOG.md: Complete documentation of all fixes
- README.md: Updated with v1.3.0 performance results
2026-01-13 01:20:20 +02:00
Manos 297edb18af v1.2 - Complete bug fix release: async folder deletion + connection timeout fixes 2026-01-12 22:55:20 +02:00
51 changed files with 16894 additions and 1380 deletions

No files matched your search

+2 -59
View File
@@ -1,60 +1,3 @@
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio cache/options
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
*.dll
*.exe
*.pdb
*.cache
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# Files built by Visual Studio
*.pidb
*.svclog
*.scc
# Caches
*.swp
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.tmp
*.log
*.bak
*.old
# PowerShell script
convert_to_ico.ps1
# Release executables (too large for git, uploaded as release assets)
release/**/PS5Upload.exe
+640
View File
@@ -0,0 +1,640 @@
# 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
**Complete stability overhaul** - Fixed all critical bugs causing connection drops, UI freezes, and upload failures. System now handles **42,801+ files** with **zero errors** and **full gigabit speeds**.
---
## 🔧 Bug Fixes
### **PAYLOAD (Server-Side) - 7 Critical Fixes**
#### 1. **Per-File Mutex Implementation**
- **Issue:** Multiple threads could write to the same file simultaneously causing data corruption
- **Fix:** Implemented per-file mutex system using hash map for file path locking
- **Impact:** Prevents race conditions during chunked uploads
#### 2. **Memory/Mutex Cleanup on Disconnect**
- **Issue:** Mutexes and memory not properly released when client disconnected
- **Fix:** Added proper cleanup in client disconnect handler
- **Impact:** Prevents memory leaks and mutex deadlocks
#### 3. **fflush() Race Condition**
- **Issue:** `fflush()` called outside file mutex, causing potential data corruption
- **Fix:** Moved `fflush()` inside mutex lock in `handle_upload_chunk`
- **Location:** `main.c:869-890`
- **Impact:** Ensures data integrity during concurrent writes
#### 4. **File Creation Race Condition**
- **Issue:** Multiple threads could attempt to create/open same file simultaneously
- **Fix:** Added `pthread_mutex_lock` around file creation and `fseeko` in `handle_start_upload`
- **Location:** `main.c:814-844`
- **Impact:** Prevents file corruption during parallel uploads
#### 5. **Global Scan Counter Issues**
- **Issue:** Static variables in recursive `count_files_recursive` caused stale values
- **Fix:** Moved `scan_count` and `last_scan_notify` to global scope
- **Location:** `main.c:324-366`
- **Impact:** Accurate progress reporting during folder deletion
#### 6. **Directory Counting Bug**
- **Issue:** `count_files_recursive` only counted files, not directories
- **Fix:** Added directory counting in recursive function
- **Location:** `main.c:350-351`
- **Impact:** Correct file count for folders with only subdirectories
#### 7. **Socket Timeout for Large Files** ⭐ **CRITICAL FIX**
- **Issue:** PS5 closed connections during large file uploads (100MB+) due to default timeout
- **Fix:** Added 5-minute socket timeout (`SO_RCVTIMEO`/`SO_SNDTIMEO`)
- **Location:** `main.c:1142-1148`
- **Impact:** **Eliminates all connection drops** - enables stable upload of 15GB+ files
- **Additional:** Aggressive TCP keepalive (10s idle, 5s interval, 3 probes)
---
### **CLIENT (Windows App) - 8 Critical Fixes**
#### 1. **ConnectAsync Timeout Memory Leak**
- **Issue:** `TcpClient` not disposed on connection timeout
- **Fix:** Added proper `Dispose()` call in timeout exception handler
- **Location:** `Protocol.cs:62-67`
- **Impact:** Prevents memory leaks during connection failures
#### 2. **ConnectAsync Exception Memory Leak**
- **Issue:** `TcpClient` not disposed on general connection exceptions
- **Fix:** Added proper `Dispose()` call in exception handler
- **Location:** `Protocol.cs:81-84`
- **Impact:** Prevents memory leaks during network errors
#### 3. **Disconnect Memory Leak**
- **Issue:** `NetworkStream` and `TcpClient` not properly disposed
- **Fix:** Added explicit `Dispose()` calls for both objects
- **Location:** `Protocol.cs:91-92`
- **Impact:** Prevents memory leaks during normal disconnection
#### 4. **Upload Deadlock During Chunked Uploads**
- **Issue:** Tasks never removed from `activeTasks` when connection index mismatched
- **Fix:** Always remove completed task regardless of connection index validity
- **Location:** `MainWindow.xaml.cs:692-734`
- **Impact:** Prevents infinite wait during parallel chunked uploads
#### 5. **Chunking Disabled for Stability** ⭐ **CRITICAL FIX**
- **Issue:** PS5 cannot handle concurrent writes to same file, even with per-file mutex
- **Fix:** Disabled chunking completely - use single connection per file
- **Location:** `MainWindow.xaml.cs:611-614`
- **Impact:** **Eliminates all "connection aborted" errors**
- **Rationale:** Even with 2 chunks, PS5 actively closes connections during concurrent writes
#### 6. **UI Freeze During Uploads**
- **Issue:** `Dispatcher.Invoke()` blocked UI thread when uploading thousands of files
- **Fix:** Replaced with `Dispatcher.InvokeAsync()` in `UpdateUploadStats`
- **Location:** `MainWindow.xaml.cs:115-158`
- **Impact:** **Fully responsive UI** during uploads
#### 7. **Log Flooding Causing UI Freeze**
- **Issue:** LogTextBox filled with hundreds of thousands of entries, causing UI slowdown
- **Fix:**
- Throttle verbose messages (log every 50th message)
- Limit log size to 1000 lines (remove oldest 200 when limit reached)
- Use `InvokeAsync` with `Background` priority
- **Location:** `MainWindow.xaml.cs:165-206`
- **Impact:** **Prevents UI freeze** and memory issues
#### 8. **Optimal Parallel Connection Strategy**
- **Issue:** Need to balance speed vs stability
- **Fix:** Use **6 parallel single-connection uploads** (no chunking)
- **Impact:**
- **Full gigabit speeds** (60-150 MB/s total throughput)
- **Zero connection drops**
- **Maximum stability**
---
## 📊 Performance Improvements
### Before Fixes:
- ❌ Connection drops every 100-200 files
- ❌ UI completely frozen during uploads
- ❌ Chunked uploads failed with "connection aborted" errors
- ❌ Memory leaks causing application slowdown
- ❌ Upload deadlocks requiring application restart
### After Fixes:
- ✅ **42,801+ files uploaded with ZERO errors**
- ✅ **Fully responsive UI** throughout entire upload
- ✅ **Consistent speeds:** 11-190 MB/s per connection
- ✅ **Total throughput:** 60-150 MB/s (full gigabit)
- ✅ **No memory leaks** - stable memory usage
- ✅ **No deadlocks** - perfect task management
---
## 🎯 Technical Architecture
### Payload (PS5 Server):
```c
// 5 minute socket timeout for large files
struct timeval tv;
tv.tv_sec = 300;
setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(client_sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
// Aggressive TCP keepalive
int keepidle = 10; // Start after 10s idle
int keepintvl = 5; // Send probe every 5s
int keepcnt = 3; // Drop after 3 failures
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
// Per-file mutex for concurrent uploads
pthread_mutex_t* file_mutex = get_file_mutex(file_path);
pthread_mutex_lock(file_mutex);
fwrite(buffer, 1, bytes_received, fp);
fflush(fp);
pthread_mutex_unlock(file_mutex);
```
### Client (Windows):
```csharp
// 6 parallel single-connection uploads
const int MaxParallelUploads = 6;
// Chunking disabled for stability
if (false) // Chunking permanently disabled
{
// PS5 cannot handle concurrent writes to same file
}
// Throttled logging for responsive UI
if (message.Contains("📊") || message.Contains("⬆️ Uploading:"))
{
_logCounter++;
if (_logCounter % 50 != 0)
return; // Skip verbose messages
}
// Async UI updates
Dispatcher.InvokeAsync(() => {
// Update UI without blocking
}, DispatcherPriority.Background);
```
---
## 🚀 Tested Configuration
### Test Case:
- **Files:** 42,801 files
- **Total Size:** ~7.52 GB
- **File Types:** .xpps, .pak, .mp4, .bk2, .bank, .json, .prx, .sprx
- **Largest File:** 15.96 GB (ac2-ps5.pak)
### Results:
- **Success Rate:** 100% (zero errors)
- **Average Speed:** 60-150 MB/s
- **Peak Speed:** 190 MB/s per connection
- **UI Responsiveness:** Fully responsive throughout
- **Memory Usage:** Stable (no leaks)
- **Completion Time:** ~1.7 hours for 42,801 files
---
## 📝 Known Limitations
1. **Chunking Disabled:** While this reduces speed for individual large files, it's necessary for PS5 stability
2. **6 Connection Limit:** More connections cause overhead without speed benefit
3. **PS5 Hardware Constraint:** PS5 cannot handle concurrent writes to same file, even with proper locking
---
## 🔄 Migration Guide
### From v1.2 to v1.3:
1. **Upload new payload** (`ps5_upload_server.elf`) to PS5
2. **Restart payload** with elfldr
3. **Use new client** (`PS5Upload.exe`)
4. **No configuration changes needed** - works out of the box
### Breaking Changes:
- None - fully backward compatible
---
## 👨‍💻 Developer Notes
### Why Chunking Was Disabled:
Multiple attempts were made to enable chunking with various configurations:
- 4 chunks: Connection drops
- 3 chunks: Connection drops
- 2 chunks: Connection drops
**Root Cause:** PS5 FreeBSD kernel actively closes TCP connections when multiple threads perform concurrent `fwrite()` operations on the same file, even with proper per-file mutex locking. The 5-minute socket timeout helps, but doesn't solve the fundamental issue of concurrent writes.
**Solution:** Single connection per file ensures only one thread writes to each file, eliminating the issue entirely.
### Why 6 Parallel Connections:
- **Less than 6:** Underutilizes network bandwidth
- **More than 6:** Increases CPU/memory overhead without speed benefit
- **6 connections:** Perfect balance for PS5 hardware
---
## 🙏 Credits
**Developed by:** Manos
**Testing:** Extensive real-world testing with 42,801+ files
**Platform:** PS5 (FreeBSD) + Windows 10/11
---
## 📄 License
This software is provided as-is for educational and personal use.
-83
View File
@@ -1,83 +0,0 @@
# PS5 Upload Protocol Specification
## Overview
Custom binary protocol for high-speed file transfers between PC and PS5.
Port: 9113
## Message Format
All messages follow this structure:
```
[Command (1 byte)][Data Length (4 bytes)][Data (variable)]
```
## Commands
### Client → Server
| Command | Value | Description | Data Format |
|---------|-------|-------------|-------------|
| PING | 0x01 | Test connection | None |
| LIST_STORAGE | 0x02 | Get storage info | None |
| LIST_DIR | 0x03 | List directory | Path (null-terminated string) |
| CREATE_DIR | 0x04 | Create directory | Path (null-terminated string) |
| DELETE_FILE | 0x05 | Delete file | Path (null-terminated string) |
| DELETE_DIR | 0x06 | Delete directory | Path (null-terminated string) |
| START_UPLOAD | 0x10 | Start file upload | Path (null-terminated) + File size (8 bytes) |
| UPLOAD_CHUNK | 0x11 | Upload file chunk | Chunk data (raw bytes) |
| END_UPLOAD | 0x12 | Finish upload | None |
| SHUTDOWN | 0xFF | Shutdown server | None |
### Server → Client
| Response | Value | Description | Data Format |
|----------|-------|-------------|-------------|
| OK | 0x01 | Success | Optional message |
| ERROR | 0x02 | Error occurred | Error message (null-terminated) |
| DATA | 0x03 | Response data | Variable format |
| READY | 0x04 | Ready for next chunk | None |
| PROGRESS | 0x05 | Upload progress | Bytes written (8 bytes) |
## Data Formats
### LIST_STORAGE Response
```
[Drive count (4 bytes)]
For each drive:
[Path length (2 bytes)][Path][Total space (8 bytes)][Free space (8 bytes)]
```
### LIST_DIR Response
```
[Entry count (4 bytes)]
For each entry:
[Type (1 byte: 0=file, 1=dir)][Name length (2 bytes)][Name][Size (8 bytes)][Timestamp (8 bytes)]
```
## Flow Examples
### Upload File
```
Client: START_UPLOAD "/data/test.bin" size=1048576
Server: READY
Client: UPLOAD_CHUNK [4MB data]
Server: PROGRESS bytes=4194304
Client: UPLOAD_CHUNK [remaining data]
Server: PROGRESS bytes=1048576
Client: END_UPLOAD
Server: OK
```
### List Directory
```
Client: LIST_DIR "/data"
Server: DATA [directory listing]
```
## Performance Optimizations
- 4MB socket buffers
- SO_NOSIGPIPE enabled
- TCP_NODELAY for low latency
- Direct disk writes (no temp files)
- Streaming chunks (no full file buffering)
+412 -94
View File
@@ -1,38 +1,133 @@
# 🚀 PS5 Upload Suite - High-Speed File Transfer
# 🚀 PS5 Upload Suite - Complete PS5 Management Platform
**By Manos**
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s** transfer speeds using a custom binary protocol.
**Version 5.0.0 - Complete Management Suite**
![PS5 Upload Client](screenshots/screenshot1.png)
![Upload in Progress](screenshots/screenshot2.png)
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.
---
## 📦 What's Included
⭐ **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!**
![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:** `payload/ps5_upload_server.elf`
- **File:** `ps5_upload_server.elf`
- **Port:** 9113
- **Protocol:** Custom binary (optimized for speed)
- **Features:**
- 4MB socket buffers
- SO_NOSIGPIPE enabled
- TCP_NODELAY for low latency
- Direct disk writes (no temp files)
- Multi-threaded client handling
- High-speed custom binary protocol
- 16MB socket buffers for maximum throughput
- Multi-threaded client handling
### 2. Windows GUI Client
- **File:** `client/bin/Release/net6.0-windows/win-x64/publish/PS5Upload.exe`
- **Framework:** .NET 6.0 WPF
- **Features:**
- Modern dark theme UI
- Drag & drop file/folder upload
- Browse PS5 filesystem
- Real-time upload progress
- Speed indicator (MB/s)
- Storage space display
- Recursive folder upload
- **File:** `PS5Upload.exe`
- 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 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`
- Upload/download files from your phone
- Multi-PS5 Profiles support
- Browse PS5 filesystem
- Favorites for quick navigation
---
@@ -40,17 +135,13 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
### Step 1: Load PS5 Payload
1. Copy `payload/ps5_upload_server.elf` to your PS5:
```
/data/etaHEN/payloads/ps5_upload_server.elf
```
1. Download `ps5_upload_server.elf` from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page
2. Load the payload with elfldr
2. Copy it to your PS5: `/data/etaHEN/payloads/ps5_upload_server.elf`
3. You should see notification:
```
PS5 Upload Server: 192.168.0.XXX:9113 - By Manos
```
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
@@ -68,81 +159,126 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
## 🎨 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, clean design
✅ **Modern UI** - Dark theme, maximized window, 8+ functional tabs
✅ **Drag & Drop** - Files and folders
✅ **Browse PS5** - Navigate filesystem
✅ **Real-time Progress** - Speed & percentage
✅ **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
✅ **💾 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
---
## 📊 Performance
| Network | Expected Speed |
|---------|----------------|
| **Gigabit Ethernet** | 100-120 MB/s |
| **WiFi 6 (5GHz)** | 60-80 MB/s |
| **WiFi 5 (5GHz)** | 40-60 MB/s |
### 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
**Note:** Speeds depend on network quality and PS5 disk performance.
### 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
## 🔧 Building from Source
| 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 |
### PS5 Payload
**Note:** Upload speeds depend on network quality and PS5 disk write performance. WiFi has ~40-60% overhead compared to Ethernet.
Requirements:
- PS5 SDK (prospero-clang)
- WSL or Linux
```bash
cd payload
bash compile.sh
```
### Windows Client
Requirements:
- .NET 6.0 SDK or later
- Windows 10/11
```cmd
cd client
build.bat
```
Or manually:
```cmd
dotnet publish -c Release -r win-x64 --self-contained true
```
---
## 📋 Protocol Specification
See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
### Commands
- `PING` - Test connection
- `LIST_STORAGE` - Get storage info
- `LIST_DIR` - List directory contents
- `CREATE_DIR` - Create directory
- `DELETE_FILE` - Delete file
- `DELETE_DIR` - Delete directory
- `START_UPLOAD` - Begin file upload
- `UPLOAD_CHUNK` - Upload file chunk
- `END_UPLOAD` - Finish upload
- `SHUTDOWN` - Shutdown server
---
@@ -174,9 +310,191 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
---
## 📝 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:
#### 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
MIT License - Free to use and modify
This software is provided as-is for personal use.
---
+53
View File
@@ -1,8 +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();
}
}
}
+1327 -126
View File
File diff suppressed because it is too large. Load diff
+3444 -724
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -16,6 +16,7 @@
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<ApplicationIcon>ps5_logo_fixed.ico</ApplicationIcon>
</PropertyGroup>
+1950 -165
View File
File diff suppressed because it is too large. Load diff
+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();
}
}
+97
View File
@@ -0,0 +1,97 @@
<?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"
xmlns:pages="clr-namespace:PS5UploadMobile.Pages"
Shell.FlyoutBehavior="Flyout"
Shell.BackgroundColor="#1E1E1E"
Shell.ForegroundColor="White"
Shell.TitleColor="White"
Shell.FlyoutBackgroundColor="#FFFFFF"
Title="PS5 Upload Suite">
<Shell.Resources>
<ResourceDictionary>
<!-- Flyout item styling: light background, dark text for maximum readability -->
<Style x:Key="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="#1E1E1E" />
<Setter Property="FontSize" Value="16" />
<Setter Property="VerticalOptions" Value="Center" />
</Style>
<!-- Flyout item template: explicit colors for text + selected background -->
<DataTemplate x:Key="FlyoutItemTemplate">
<Grid HeightRequest="54" Padding="16,0" ColumnDefinitions="Auto,*">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter TargetName="FlyoutRoot" Property="Grid.BackgroundColor" Value="#FFFFFF" />
<Setter TargetName="FlyoutText" Property="Label.TextColor" Value="#1E1E1E" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter TargetName="FlyoutRoot" Property="Grid.BackgroundColor" Value="#E3F2FD" />
<Setter TargetName="FlyoutText" Property="Label.TextColor" Value="#0078D4" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="FlyoutRoot" Grid.ColumnSpan="2" BackgroundColor="#FFFFFF" />
<Image Grid.Column="0" Source="{Binding FlyoutIcon}" Margin="0,0,12,0"
VerticalOptions="Center" HeightRequest="22" WidthRequest="22"/>
<Label x:Name="FlyoutText" Grid.Column="1" Text="{Binding Title}"
FontSize="16" FontAttributes="Bold"
TextColor="#1E1E1E"
VerticalOptions="Center" />
</Grid>
</DataTemplate>
<!-- Apply to all FlyoutItems -->
<Style TargetType="FlyoutItem">
<Setter Property="Shell.ItemTemplate" Value="{StaticResource FlyoutItemTemplate}" />
</Style>
</ResourceDictionary>
</Shell.Resources>
<Shell.FlyoutHeader>
<Grid HeightRequest="130" BackgroundColor="#0078D4">
<VerticalStackLayout VerticalOptions="Center" HorizontalOptions="Center" Spacing="4">
<Label Text="🎮 PS5 Upload Suite" FontSize="22" FontAttributes="Bold" TextColor="White" HorizontalOptions="Center"/>
<Label Text="Complete Management" FontSize="13" TextColor="White" Opacity="0.9" HorizontalOptions="Center"/>
</VerticalStackLayout>
</Grid>
</Shell.FlyoutHeader>
<FlyoutItem Title="Files" Icon="{OnPlatform Default='folder.png'}">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="files" />
</FlyoutItem>
<FlyoutItem Title="Games">
<ShellContent ContentTemplate="{DataTemplate pages:GamesPage}" Route="games" />
</FlyoutItem>
<FlyoutItem Title="Saves">
<ShellContent ContentTemplate="{DataTemplate pages:SavesPage}" Route="saves" />
</FlyoutItem>
<FlyoutItem Title="Screenshots">
<ShellContent ContentTemplate="{DataTemplate pages:ScreenshotsPage}" Route="screenshots" />
</FlyoutItem>
<FlyoutItem Title="Hardware">
<ShellContent ContentTemplate="{DataTemplate pages:HardwarePage}" Route="hardware" />
</FlyoutItem>
<FlyoutItem Title="Shell">
<ShellContent ContentTemplate="{DataTemplate pages:ShellPage}" Route="shell" />
</FlyoutItem>
<FlyoutItem Title="Mount Games">
<ShellContent ContentTemplate="{DataTemplate pages:MountGamesPage}" Route="mount" />
</FlyoutItem>
</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>
+577
View File
@@ -0,0 +1,577 @@
#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();
// Also establish shared protocol connection for other tabs (Games, Saves, Screenshots, Hardware, Shell, Mount)
int portNum = int.TryParse(PortEntry.Text, out var p) ? p : 9113;
bool sharedOk = await ConnectionManager.Instance.ConnectAsync(IpEntry.Text, portNum);
AddLog(sharedOk ? "Shared protocol connected (all tabs ready)" : "Shared protocol connection failed — other tabs may not work");
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();
ConnectionManager.Instance.Disconnect();
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();
}
}
+52
View File
@@ -0,0 +1,52 @@
<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>
<!-- Suppress NETSDK1202 (EOL workload warning for .NET 6 Android) -->
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<NoWarn>$(NoWarn);NETSDK1202</NoWarn>
</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>
+49
View File
@@ -0,0 +1,49 @@
<?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.Pages.GamesPage"
BackgroundColor="#121212"
Title="Games">
<Grid RowDefinitions="Auto,Auto,*" Padding="10">
<!-- Toolbar -->
<HorizontalStackLayout Grid.Row="0" Spacing="8">
<Button Text="🔄 Refresh" Clicked="OnRefreshClicked" BackgroundColor="#0078D4" TextColor="White" FontSize="14"/>
<Button Text="🎮 Mount All" Clicked="OnMountAllClicked" BackgroundColor="#107C10" TextColor="White" FontSize="14"/>
</HorizontalStackLayout>
<!-- Status Label -->
<Label Grid.Row="1" x:Name="StatusLabel" Text="Tap Refresh to load games"
TextColor="Gray" FontSize="12" Margin="4,6"/>
<!-- Games List -->
<RefreshView Grid.Row="2" x:Name="GamesRefresh" Refreshing="OnRefreshing">
<CollectionView x:Name="GamesListView" SelectionMode="Single"
SelectionChanged="OnGameSelected">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10,8" ColumnDefinitions="80,*,Auto" ColumnSpacing="10" BackgroundColor="#1E1E1E" Margin="0,0,0,4">
<!-- Icon -->
<Frame Grid.Column="0" Padding="0" CornerRadius="6" IsClippedToBounds="True"
BackgroundColor="#333" WidthRequest="80" HeightRequest="80" HasShadow="False">
<Image Source="{Binding Icon}" Aspect="AspectFill" WidthRequest="80" HeightRequest="80"/>
</Frame>
<!-- Info -->
<VerticalStackLayout Grid.Column="1" VerticalOptions="Center" Spacing="2">
<Label Text="{Binding Name}" TextColor="White" FontSize="16" FontAttributes="Bold" LineBreakMode="TailTruncation"/>
<Label Text="{Binding TitleId}" TextColor="#80B0FF" FontSize="12"/>
<Label Text="{Binding SizeDisplay}" TextColor="Gray" FontSize="12"/>
<Label Text="{Binding Region}" TextColor="Gray" FontSize="11"/>
</VerticalStackLayout>
<!-- Status -->
<Label Grid.Column="2" Text="{Binding StatusDisplay}" TextColor="LimeGreen" FontSize="11" VerticalOptions="Center"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.EmptyView>
<Label Text="No games found. Pull to refresh." TextColor="Gray" HorizontalOptions="Center" VerticalOptions="Center"/>
</CollectionView.EmptyView>
</CollectionView>
</RefreshView>
</Grid>
</ContentPage>
+164
View File
@@ -0,0 +1,164 @@
#nullable enable
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using PS5Upload;
namespace PS5UploadMobile.Pages;
public partial class GamesPage : ContentPage
{
private readonly ObservableCollection<PS5MountedGame> _games = new();
public GamesPage()
{
InitializeComponent();
GamesListView.ItemsSource = _games;
}
protected override async void OnAppearing()
{
base.OnAppearing();
if (_games.Count == 0) await LoadGamesAsync();
}
private async void OnRefreshClicked(object sender, EventArgs e) => await LoadGamesAsync();
private async void OnRefreshing(object sender, EventArgs e)
{
await LoadGamesAsync();
GamesRefresh.IsRefreshing = false;
}
private async Task LoadGamesAsync()
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
StatusLabel.Text = "Loading games…";
try
{
var list = await proto.GetGameListAsync();
_games.Clear();
foreach (var g in list) _games.Add(g);
StatusLabel.Text = $"Loaded {_games.Count} games";
// Load icons in background
_ = Task.Run(async () =>
{
foreach (var game in list)
{
try
{
byte[]? iconBytes = await proto.GetGameIconAsync(game.TitleId);
if (iconBytes != null && iconBytes.Length > 0)
{
var ms = new MemoryStream(iconBytes);
await MainThread.InvokeOnMainThreadAsync(() =>
{
game.Icon = ImageSource.FromStream(() => new MemoryStream(iconBytes));
});
}
}
catch { }
}
});
}
catch (Exception ex)
{
StatusLabel.Text = $"Error: {ex.Message}";
}
}
private async void OnGameSelected(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection == null || e.CurrentSelection.Count == 0) return;
if (e.CurrentSelection[0] is not PS5MountedGame game) return;
GamesListView.SelectedItem = null;
string action = await DisplayActionSheet($"{game.Name}", "Cancel", null,
"▶️ Launch Game",
"📖 Game Details",
"⏏️ Unmount Game",
"📋 Copy Title ID");
switch (action)
{
case "▶️ Launch Game":
await LaunchGameAsync(game);
break;
case "📖 Game Details":
await ShowGameDetailsAsync(game);
break;
case "⏏️ Unmount Game":
await UnmountGameAsync(game);
break;
case "📋 Copy Title ID":
await Clipboard.SetTextAsync(game.TitleId);
await DisplayAlert("Copied", $"Copied {game.TitleId}", "OK");
break;
}
}
private async Task LaunchGameAsync(PS5MountedGame game)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
bool confirm = await DisplayAlert("Launch Game", $"Launch '{game.Name}' on your PS5?", "Launch", "Cancel");
if (!confirm) return;
var (ok, msg) = await proto.LaunchGameAsync(game.TitleId);
await DisplayAlert(ok ? "Launching" : "Error", msg, "OK");
}
private async Task UnmountGameAsync(PS5MountedGame game)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
bool confirm = await DisplayAlert("Unmount Game", $"Unmount '{game.Name}' from PS5?", "Unmount", "Cancel");
if (!confirm) return;
var (ok, msg) = await proto.UnmountGameAsync(game.TitleId);
await DisplayAlert(ok ? "Unmounted" : "Error", msg, "OK");
if (ok) await LoadGamesAsync();
}
private async Task ShowGameDetailsAsync(PS5MountedGame game)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
StatusLabel.Text = "Loading game details…";
var details = await proto.GetGameDetailsAsync(game.TitleId);
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Title: {game.Name}");
sb.AppendLine($"Title ID: {game.TitleId}");
sb.AppendLine($"Region: {game.Region}");
sb.AppendLine($"Size: {game.SizeDisplay}");
sb.AppendLine($"Path: {game.Path}");
sb.AppendLine($"Status: {game.StatusDisplay}");
if (details != null)
{
sb.AppendLine();
sb.AppendLine("—— param.json ——");
foreach (var kv in details) sb.AppendLine($"{kv.Key}: {kv.Value}");
}
StatusLabel.Text = $"Loaded {_games.Count} games";
await DisplayAlert("Game Details", sb.ToString(), "Close");
}
private async void OnMountAllClicked(object sender, EventArgs e)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
bool confirm = await DisplayAlert("Mount Games",
"This will scan all known locations (internal, USB, M.2) and mount unmounted games. Continue?",
"Mount", "Cancel");
if (!confirm) return;
StatusLabel.Text = "Mounting games (this can take a while)…";
var (ok, summary) = await proto.MountGamesAsync(msg => MainThread.BeginInvokeOnMainThread(() => StatusLabel.Text = msg));
StatusLabel.Text = ok ? "Mount complete" : "Mount failed";
await DisplayAlert(ok ? "Mount Games - Done" : "Mount Games - Error", summary, "OK");
if (ok) await LoadGamesAsync();
}
}
+102
View File
@@ -0,0 +1,102 @@
<?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.Pages.HardwarePage"
BackgroundColor="#121212"
Title="Hardware">
<ScrollView>
<VerticalStackLayout Padding="12" Spacing="14">
<!-- Toolbar -->
<HorizontalStackLayout Spacing="10">
<Button Text="🔄 Refresh" Clicked="OnRefreshClicked" BackgroundColor="#0078D4" TextColor="White"/>
<HorizontalStackLayout Spacing="6" VerticalOptions="Center">
<CheckBox x:Name="AutoRefreshCheck" Color="#0078D4" CheckedChanged="OnAutoRefreshChanged"/>
<Label Text="Auto-refresh (5s)" TextColor="White" VerticalOptions="Center"/>
</HorizontalStackLayout>
</HorizontalStackLayout>
<Label x:Name="StatusLabel" Text="Tap Refresh to load hardware info" TextColor="Gray" FontSize="12"/>
<!-- System Info -->
<Frame BackgroundColor="#1E1E1E" BorderColor="#2D2D2D" Padding="14" CornerRadius="8" HasShadow="False">
<VerticalStackLayout Spacing="8">
<Label Text="🖥️ System Info" TextColor="#80B0FF" FontSize="16" FontAttributes="Bold"/>
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnSpacing="10" RowSpacing="6">
<Label Grid.Row="0" Grid.Column="0" Text="Model" TextColor="Gray"/>
<Label Grid.Row="0" Grid.Column="1" x:Name="ModelLabel" Text="—" TextColor="White"/>
<Label Grid.Row="1" Grid.Column="0" Text="Serial" TextColor="Gray"/>
<Label Grid.Row="1" Grid.Column="1" x:Name="SerialLabel" Text="—" TextColor="White"/>
<Label Grid.Row="2" Grid.Column="0" Text="Machine" TextColor="Gray"/>
<Label Grid.Row="2" Grid.Column="1" x:Name="MachineLabel" Text="—" TextColor="White"/>
<Label Grid.Row="3" Grid.Column="0" Text="OS" TextColor="Gray"/>
<Label Grid.Row="3" Grid.Column="1" x:Name="OsLabel" Text="—" TextColor="White"/>
<Label Grid.Row="4" Grid.Column="0" Text="CPU Cores" TextColor="Gray"/>
<Label Grid.Row="4" Grid.Column="1" x:Name="CpuCoresLabel" Text="—" TextColor="White"/>
<Label Grid.Row="5" Grid.Column="0" Text="Physical RAM" TextColor="Gray"/>
<Label Grid.Row="5" Grid.Column="1" x:Name="RamLabel" Text="—" TextColor="White"/>
<Label Grid.Row="6" Grid.Column="0" Text="Wi-Fi / BT" TextColor="Gray"/>
<Label Grid.Row="6" Grid.Column="1" x:Name="WlanLabel" Text="—" TextColor="White"/>
<Label Grid.Row="7" Grid.Column="0" Text="Optical Out" TextColor="Gray"/>
<Label Grid.Row="7" Grid.Column="1" x:Name="OpticalLabel" Text="—" TextColor="White"/>
</Grid>
</VerticalStackLayout>
</Frame>
<!-- Live Sensors -->
<Frame BackgroundColor="#1E1E1E" BorderColor="#2D2D2D" Padding="14" CornerRadius="8" HasShadow="False">
<VerticalStackLayout Spacing="12">
<Label Text="🔥 Live Sensors" TextColor="#FF9050" FontSize="16" FontAttributes="Bold"/>
<VerticalStackLayout Spacing="4">
<Grid ColumnDefinitions="*,Auto">
<Label Text="CPU Temperature" TextColor="White"/>
<Label Grid.Column="1" x:Name="CpuTempLabel" Text="—" TextColor="Orange" FontAttributes="Bold"/>
</Grid>
<ProgressBar x:Name="CpuTempBar" Progress="0" ProgressColor="#FF9050"/>
</VerticalStackLayout>
<VerticalStackLayout Spacing="4">
<Grid ColumnDefinitions="*,Auto">
<Label Text="SoC Temperature" TextColor="White"/>
<Label Grid.Column="1" x:Name="SocTempLabel" Text="—" TextColor="Orange" FontAttributes="Bold"/>
</Grid>
<ProgressBar x:Name="SocTempBar" Progress="0" ProgressColor="#FF5050"/>
</VerticalStackLayout>
<VerticalStackLayout Spacing="4">
<Grid ColumnDefinitions="*,Auto">
<Label Text="CPU Frequency" TextColor="White"/>
<Label Grid.Column="1" x:Name="CpuFreqLabel" Text="—" TextColor="LimeGreen" FontAttributes="Bold"/>
</Grid>
<ProgressBar x:Name="CpuFreqBar" Progress="0" ProgressColor="LimeGreen"/>
</VerticalStackLayout>
<VerticalStackLayout Spacing="4">
<Grid ColumnDefinitions="*,Auto">
<Label Text="SoC Power" TextColor="White"/>
<Label Grid.Column="1" x:Name="SocPowerLabel" Text="—" TextColor="Yellow" FontAttributes="Bold"/>
</Grid>
<ProgressBar x:Name="SocPowerBar" Progress="0" ProgressColor="Yellow"/>
</VerticalStackLayout>
</VerticalStackLayout>
</Frame>
<!-- Power Info -->
<Frame BackgroundColor="#1E1E1E" BorderColor="#2D2D2D" Padding="14" CornerRadius="8" HasShadow="False">
<VerticalStackLayout Spacing="8">
<Label Text="⚡ Power Stats" TextColor="#FFD050" FontSize="16" FontAttributes="Bold"/>
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="10" RowSpacing="6">
<Label Grid.Row="0" Grid.Column="0" Text="Operating Time" TextColor="Gray"/>
<Label Grid.Row="0" Grid.Column="1" x:Name="OpTimeLabel" Text="—" TextColor="White"/>
<Label Grid.Row="1" Grid.Column="0" Text="Boot Count" TextColor="Gray"/>
<Label Grid.Row="1" Grid.Column="1" x:Name="BootCountLabel" Text="—" TextColor="White"/>
<Label Grid.Row="2" Grid.Column="0" Text="Total Power" TextColor="Gray"/>
<Label Grid.Row="2" Grid.Column="1" x:Name="PowerConsLabel" Text="—" TextColor="White"/>
</Grid>
</VerticalStackLayout>
</Frame>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
+129
View File
@@ -0,0 +1,129 @@
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Dispatching;
using PS5Upload;
namespace PS5UploadMobile.Pages;
public partial class HardwarePage : ContentPage
{
private IDispatcherTimer? _autoTimer;
private int _busy; // Interlocked guard
public HardwarePage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
await RefreshAllAsync();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
StopAutoTimer();
}
private async void OnRefreshClicked(object sender, EventArgs e) => await RefreshAllAsync();
private void OnAutoRefreshChanged(object sender, CheckedChangedEventArgs e)
{
if (e.Value) StartAutoTimer();
else StopAutoTimer();
}
private void StartAutoTimer()
{
StopAutoTimer();
_autoTimer = Dispatcher.CreateTimer();
_autoTimer.Interval = TimeSpan.FromSeconds(5);
_autoTimer.Tick += async (_, __) => await RefreshSensorsAsync();
_autoTimer.Start();
}
private void StopAutoTimer()
{
if (_autoTimer != null) { _autoTimer.Stop(); _autoTimer = null; }
}
private async Task RefreshAllAsync()
{
if (Interlocked.Exchange(ref _busy, 1) == 1) return;
try
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) { StatusLabel.Text = "Disconnected"; return; }
StatusLabel.Text = "Loading hardware info…";
var hw = await proto.GetHardwareInfoAsync();
if (hw != null)
{
ModelLabel.Text = hw.Model;
SerialLabel.Text = hw.Serial;
MachineLabel.Text = hw.HwMachine;
OsLabel.Text = hw.OsVersion;
CpuCoresLabel.Text = hw.NumCpu > 0 ? hw.NumCpu.ToString() : "—";
RamLabel.Text = hw.PhysMemDisplay;
WlanLabel.Text = hw.WlanBtDisplay;
OpticalLabel.Text = hw.OpticalDisplay;
}
await UpdateSensorsAsync(proto);
await UpdatePowerAsync(proto);
StatusLabel.Text = $"Updated {DateTime.Now:HH:mm:ss}";
}
catch (Exception ex) { StatusLabel.Text = $"Error: {ex.Message}"; }
finally { Interlocked.Exchange(ref _busy, 0); }
}
private async Task RefreshSensorsAsync()
{
if (Interlocked.Exchange(ref _busy, 1) == 1) return;
try
{
var proto = PageHelper.Conn.Protocol;
if (proto == null || !proto.IsConnected) { StopAutoTimer(); return; }
await UpdateSensorsAsync(proto);
StatusLabel.Text = $"Updated {DateTime.Now:HH:mm:ss}";
}
catch { }
finally { Interlocked.Exchange(ref _busy, 0); }
}
private async Task UpdateSensorsAsync(PS5Protocol proto)
{
var temps = await proto.GetTemperatureInfoAsync();
if (temps == null) return;
CpuTempLabel.Text = temps.CpuTempDisplay;
SocTempLabel.Text = temps.SocTempDisplay;
CpuFreqLabel.Text = temps.CpuFreqDisplay;
SocPowerLabel.Text = temps.SocPowerDisplay;
// Progress bars - map sensible ranges
CpuTempBar.Progress = Math.Clamp(temps.CpuTemp / 100.0, 0, 1);
SocTempBar.Progress = Math.Clamp(temps.SocTemp / 100.0, 0, 1);
// PS5 CPU typical 3.5 GHz max
CpuFreqBar.Progress = Math.Clamp(temps.CpuFreqMhz / 3500.0, 0, 1);
// SoC power 0–200W range
SocPowerBar.Progress = Math.Clamp(temps.SocPowerMw / 200000.0, 0, 1);
}
private async Task UpdatePowerAsync(PS5Protocol proto)
{
var power = await proto.GetPowerInfoAsync();
if (power == null) return;
OpTimeLabel.Text = $"{power.OperatingTimeHours}h {power.OperatingTimeMinutes}m";
BootCountLabel.Text = power.BootCount.ToString();
PowerConsLabel.Text = power.PowerConsumptionMw > 0
? $"{power.PowerConsumptionMw / 1000.0:F2} kW·s (lifetime)"
: "—";
}
}
+28
View File
@@ -0,0 +1,28 @@
<?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.Pages.MountGamesPage"
BackgroundColor="#121212"
Title="Mount Games">
<Grid RowDefinitions="Auto,Auto,*" Padding="14">
<Frame Grid.Row="0" BackgroundColor="#1E1E1E" BorderColor="#2D2D2D" Padding="14" CornerRadius="8" HasShadow="False">
<VerticalStackLayout Spacing="6">
<Label Text="🎮 Mount Games" TextColor="#80B0FF" FontSize="18" FontAttributes="Bold"/>
<Label Text="Scan all known locations (internal, USB 0–3, M.2 SSD) and automatically mount any unmounted games onto the PS5 home screen."
TextColor="LightGray" FontSize="12"/>
</VerticalStackLayout>
</Frame>
<Button Grid.Row="1" x:Name="StartBtn" Text="🚀 Start Mount Scan"
Clicked="OnStartClicked" BackgroundColor="#107C10" TextColor="White"
FontAttributes="Bold" HeightRequest="55" Margin="0,12,0,12"/>
<Border Grid.Row="2" Stroke="#333" StrokeThickness="1" Background="#000" Padding="8">
<ScrollView x:Name="LogScroll">
<Label x:Name="LogLabel" Text="Ready. Tap 'Start Mount Scan' to begin."
TextColor="#00FF40" FontFamily="Monospace" FontSize="12"/>
</ScrollView>
</Border>
</Grid>
</ContentPage>
+48
View File
@@ -0,0 +1,48 @@
#nullable enable
using System;
using System.Text;
using Microsoft.Maui.Controls;
namespace PS5UploadMobile.Pages;
public partial class MountGamesPage : ContentPage
{
private readonly StringBuilder _buffer = new();
public MountGamesPage()
{
InitializeComponent();
}
private void AppendLine(string s)
{
_buffer.AppendLine(s);
LogLabel.Text = _buffer.ToString();
MainThread.BeginInvokeOnMainThread(async () =>
{
await System.Threading.Tasks.Task.Delay(50);
await LogScroll.ScrollToAsync(0, double.MaxValue, false);
});
}
private async void OnStartClicked(object sender, EventArgs e)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
StartBtn.IsEnabled = false;
_buffer.Clear();
AppendLine("🚀 Starting mount scan…");
AppendLine("Scanning: /data/etaHEN/games, /mnt/usb0-3, /mnt/ext0/games …");
var (ok, summary) = await proto.MountGamesAsync(msg =>
MainThread.BeginInvokeOnMainThread(() => AppendLine(msg)));
AppendLine("");
AppendLine(ok ? "✅ Mount scan complete" : "❌ Mount scan failed");
AppendLine("");
AppendLine("—— Summary ——");
AppendLine(summary);
StartBtn.IsEnabled = true;
}
}
+48
View File
@@ -0,0 +1,48 @@
#nullable enable
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using PS5Upload;
using PS5UploadMobile.Services;
namespace PS5UploadMobile.Pages
{
internal static class PageHelper
{
public static ConnectionManager Conn => ConnectionManager.Instance;
public static async Task<PS5Protocol?> EnsureConnectedAsync(Page page)
{
var proto = await Conn.EnsureConnectedAsync();
if (proto == null)
{
await page.DisplayAlert("Not Connected",
$"Could not connect to {Conn.IpAddress}:{Conn.Port}. Please connect from the Files tab first.",
"OK");
}
return proto;
}
public static string DownloadsDir
{
get
{
string? root = Microsoft.Maui.Storage.FileSystem.AppDataDirectory;
string path = Path.Combine(root, "PS5Downloads");
Directory.CreateDirectory(path);
return path;
}
}
public static string ThumbnailCacheDir
{
get
{
string path = Path.Combine(Microsoft.Maui.Storage.FileSystem.CacheDirectory, "thumbnails");
Directory.CreateDirectory(path);
return path;
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?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.Pages.SavesPage"
BackgroundColor="#121212"
Title="Saves">
<Grid RowDefinitions="Auto,Auto,*" Padding="10">
<HorizontalStackLayout Grid.Row="0" Spacing="8">
<Button Text="🔄 Refresh" Clicked="OnRefreshClicked" BackgroundColor="#0078D4" TextColor="White" FontSize="14"/>
</HorizontalStackLayout>
<Label Grid.Row="1" x:Name="StatusLabel" Text="Tap Refresh to load save files"
TextColor="Gray" FontSize="12" Margin="4,6"/>
<RefreshView Grid.Row="2" x:Name="SavesRefresh" Refreshing="OnRefreshing">
<CollectionView x:Name="SavesListView" SelectionMode="Single"
SelectionChanged="OnSaveSelected">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10,8" ColumnDefinitions="60,*,Auto" ColumnSpacing="10"
BackgroundColor="#1E1E1E" Margin="0,0,0,4">
<Frame Grid.Column="0" Padding="0" CornerRadius="6" IsClippedToBounds="True"
BackgroundColor="#333" WidthRequest="60" HeightRequest="60" HasShadow="False">
<Image Source="{Binding Icon}" Aspect="AspectFill"/>
</Frame>
<VerticalStackLayout Grid.Column="1" VerticalOptions="Center" Spacing="2">
<Label Text="{Binding GameName}" TextColor="White" FontSize="15" FontAttributes="Bold"
LineBreakMode="TailTruncation"/>
<Label Text="{Binding TitleId}" TextColor="#80B0FF" FontSize="11"/>
<Label TextColor="Gray" FontSize="10">
<Label.FormattedText>
<FormattedString>
<Span Text="User: "/>
<Span Text="{Binding ShortUserId}"/>
<Span Text=" • "/>
<Span Text="{Binding ModifiedDisplay}"/>
</FormattedString>
</Label.FormattedText>
</Label>
</VerticalStackLayout>
<Label Grid.Column="2" Text="{Binding SizeDisplay}" TextColor="Orange" FontSize="12"
VerticalOptions="Center"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.EmptyView>
<Label Text="No saves found." TextColor="Gray" HorizontalOptions="Center" VerticalOptions="Center"/>
</CollectionView.EmptyView>
</CollectionView>
</RefreshView>
</Grid>
</ContentPage>
+115
View File
@@ -0,0 +1,115 @@
#nullable enable
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using PS5Upload;
namespace PS5UploadMobile.Pages;
public partial class SavesPage : ContentPage
{
private readonly ObservableCollection<PS5SaveGame> _saves = new();
private readonly System.Collections.Generic.Dictionary<string, string> _gameNameCache = new();
public SavesPage()
{
InitializeComponent();
SavesListView.ItemsSource = _saves;
}
protected override async void OnAppearing()
{
base.OnAppearing();
if (_saves.Count == 0) await LoadSavesAsync();
}
private async void OnRefreshClicked(object sender, EventArgs e) => await LoadSavesAsync();
private async void OnRefreshing(object sender, EventArgs e)
{
await LoadSavesAsync();
SavesRefresh.IsRefreshing = false;
}
private async Task LoadSavesAsync()
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
StatusLabel.Text = "Loading saves…";
try
{
var list = await proto.ListSavesAsync();
_saves.Clear();
foreach (var s in list)
{
s.GameName = _gameNameCache.TryGetValue(s.TitleId, out var nm) ? nm : s.TitleId;
_saves.Add(s);
}
StatusLabel.Text = $"Loaded {_saves.Count} saves";
// Background: fetch game names from GetGameList + icons
_ = Task.Run(async () =>
{
try
{
var games = await proto.GetGameListAsync();
var gameDict = new System.Collections.Generic.Dictionary<string, string>();
foreach (var g in games) gameDict[g.TitleId] = g.Name;
foreach (var save in list)
{
if (gameDict.TryGetValue(save.TitleId, out var nm))
{
_gameNameCache[save.TitleId] = nm;
await MainThread.InvokeOnMainThreadAsync(() => save.GameName = nm);
}
byte[]? iconBytes = await proto.GetGameIconAsync(save.TitleId);
if (iconBytes != null && iconBytes.Length > 0)
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
save.Icon = ImageSource.FromStream(() => new MemoryStream(iconBytes));
});
}
}
}
catch { }
});
}
catch (Exception ex)
{
StatusLabel.Text = $"Error: {ex.Message}";
}
}
private async void OnSaveSelected(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection == null || e.CurrentSelection.Count == 0) return;
if (e.CurrentSelection[0] is not PS5SaveGame save) return;
SavesListView.SelectedItem = null;
string action = await DisplayActionSheet($"{save.GameName} ({save.TitleId})", "Cancel", null,
"📋 Copy Save Path",
"ℹ️ Save Info");
if (action == "📋 Copy Save Path")
{
await Clipboard.SetTextAsync(save.SavePath);
await DisplayAlert("Copied", $"Copied: {save.SavePath}", "OK");
}
else if (action == "ℹ️ Save Info")
{
string info =
$"Game: {save.GameName}\n" +
$"Title ID: {save.TitleId}\n" +
$"User ID: {save.UserId}\n" +
$"Size: {save.SizeDisplay}\n" +
$"Modified: {save.ModifiedDisplay}\n" +
$"Path: {save.SavePath}";
await DisplayAlert("Save Details", info, "Close");
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?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.Pages.ScreenshotsPage"
BackgroundColor="#121212"
Title="Screenshots">
<Grid RowDefinitions="Auto,Auto,*" Padding="10">
<HorizontalStackLayout Grid.Row="0" Spacing="8">
<Button Text="🔄 Refresh" Clicked="OnRefreshClicked" BackgroundColor="#0078D4" TextColor="White" FontSize="14"/>
</HorizontalStackLayout>
<Label Grid.Row="1" x:Name="StatusLabel" Text="Tap Refresh to load screenshots"
TextColor="Gray" FontSize="12" Margin="4,6"/>
<RefreshView Grid.Row="2" x:Name="ShotsRefresh" Refreshing="OnRefreshing">
<CollectionView x:Name="ShotsListView" SelectionMode="Single"
SelectionChanged="OnShotSelected">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="2" VerticalItemSpacing="6" HorizontalItemSpacing="6"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Padding="0" CornerRadius="8" IsClippedToBounds="True"
BackgroundColor="#1E1E1E" HasShadow="False" BorderColor="#333">
<Grid RowDefinitions="*,Auto">
<Image Grid.Row="0" Source="{Binding Thumbnail}" Aspect="AspectFill" HeightRequest="130"
BackgroundColor="#2A2A2A"/>
<VerticalStackLayout Grid.Row="1" Padding="6,4" Spacing="0">
<Label Text="{Binding FileName}" TextColor="White" FontSize="11"
LineBreakMode="TailTruncation"/>
<Label TextColor="Gray" FontSize="10">
<Label.FormattedText>
<FormattedString>
<Span Text="{Binding SizeDisplay}"/>
<Span Text=" • "/>
<Span Text="{Binding DateDisplay}"/>
</FormattedString>
</Label.FormattedText>
</Label>
</VerticalStackLayout>
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.EmptyView>
<Label Text="No screenshots found. Pull to refresh." TextColor="Gray"
HorizontalOptions="Center" VerticalOptions="Center"/>
</CollectionView.EmptyView>
</CollectionView>
</RefreshView>
</Grid>
</ContentPage>
+173
View File
@@ -0,0 +1,173 @@
#nullable enable
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using PS5Upload;
namespace PS5UploadMobile.Pages;
public partial class ScreenshotsPage : ContentPage
{
private readonly ObservableCollection<PS5Screenshot> _shots = new();
public ScreenshotsPage()
{
InitializeComponent();
ShotsListView.ItemsSource = _shots;
}
protected override async void OnAppearing()
{
base.OnAppearing();
if (_shots.Count == 0) await LoadShotsAsync();
}
private async void OnRefreshClicked(object sender, EventArgs e) => await LoadShotsAsync();
private async void OnRefreshing(object sender, EventArgs e)
{
await LoadShotsAsync();
ShotsRefresh.IsRefreshing = false;
}
private async Task LoadShotsAsync()
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
StatusLabel.Text = "Loading screenshots…";
try
{
var list = await proto.ListScreenshotsAsync();
_shots.Clear();
foreach (var s in list) _shots.Add(s);
StatusLabel.Text = $"Loaded {_shots.Count} screenshots";
// Load thumbnails in background
_ = Task.Run(() => LoadThumbnailsAsync(list));
}
catch (Exception ex)
{
StatusLabel.Text = $"Error: {ex.Message}";
}
}
private async Task LoadThumbnailsAsync(System.Collections.Generic.List<PS5Screenshot> shots)
{
string cacheDir = PageHelper.ThumbnailCacheDir;
foreach (var shot in shots)
{
try
{
// Cache key
string hash;
using (var md5 = MD5.Create())
{
var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(shot.FullPath));
hash = BitConverter.ToString(bytes).Replace("-", "").ToLower();
}
string cacheFile = Path.Combine(cacheDir, hash + ".jpg");
if (!File.Exists(cacheFile))
{
// Need its own protocol - use shared connection
var proto = PageHelper.Conn.Protocol;
if (proto == null || !proto.IsConnected) return;
bool ok = await proto.DownloadFileAsync(shot.FullPath, cacheFile);
if (!ok) continue;
}
await MainThread.InvokeOnMainThreadAsync(() =>
{
shot.Thumbnail = ImageSource.FromFile(cacheFile);
});
}
catch { }
}
}
private async void OnShotSelected(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection == null || e.CurrentSelection.Count == 0) return;
if (e.CurrentSelection[0] is not PS5Screenshot shot) return;
ShotsListView.SelectedItem = null;
string action = await DisplayActionSheet(shot.FileName, "Cancel", null,
"👁️ View",
"⬇️ Download",
"🗑️ Delete",
"📋 Copy Path");
switch (action)
{
case "👁️ View":
await ViewShotAsync(shot);
break;
case "⬇️ Download":
await DownloadShotAsync(shot);
break;
case "🗑️ Delete":
await DeleteShotAsync(shot);
break;
case "📋 Copy Path":
await Clipboard.SetTextAsync(shot.FullPath);
await DisplayAlert("Copied", shot.FullPath, "OK");
break;
}
}
private async Task ViewShotAsync(PS5Screenshot shot)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
StatusLabel.Text = "Downloading for preview…";
string tmp = Path.Combine(PageHelper.ThumbnailCacheDir, "preview_" + Path.GetFileName(shot.FileName));
bool ok = await proto.DownloadFileAsync(shot.FullPath, tmp);
StatusLabel.Text = $"Loaded {_shots.Count} screenshots";
if (!ok) { await DisplayAlert("Error", "Download failed", "OK"); return; }
try
{
await Microsoft.Maui.ApplicationModel.Launcher.OpenAsync(
new Microsoft.Maui.ApplicationModel.OpenFileRequest { File = new ReadOnlyFile(tmp) });
}
catch (Exception ex) { await DisplayAlert("Open failed", ex.Message, "OK"); }
}
private async Task DownloadShotAsync(PS5Screenshot shot)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
string dir = PageHelper.DownloadsDir;
string local = Path.Combine(dir, shot.FileName);
StatusLabel.Text = $"Downloading {shot.FileName}…";
bool ok = await proto.DownloadFileAsync(shot.FullPath, local);
StatusLabel.Text = $"Loaded {_shots.Count} screenshots";
await DisplayAlert(ok ? "Downloaded" : "Error",
ok ? $"Saved to:\n{local}" : "Download failed", "OK");
}
private async Task DeleteShotAsync(PS5Screenshot shot)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
bool confirm = await DisplayAlert("Delete Screenshot",
$"Delete {shot.FileName}? This also removes the thumbnail from PS5 Media Gallery.",
"Delete", "Cancel");
if (!confirm) return;
var (ok, msg) = await proto.DeleteScreenshotAsync(shot.FullPath);
if (ok)
{
_shots.Remove(shot);
StatusLabel.Text = $"Deleted. {_shots.Count} screenshots remaining.";
}
else
{
await DisplayAlert("Delete failed", msg, "OK");
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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.Pages.ShellPage"
BackgroundColor="#121212"
Title="Shell">
<Grid RowDefinitions="Auto,*,Auto" Padding="10">
<HorizontalStackLayout Grid.Row="0" Spacing="8" Margin="0,0,0,6">
<Button Text="🔗 Open Shell" Clicked="OnOpenClicked" BackgroundColor="#107C10" TextColor="White" FontSize="13"/>
<Button Text="❌ Close" Clicked="OnCloseClicked" BackgroundColor="#D13438" TextColor="White" FontSize="13"/>
<Button Text="🧹 Clear" Clicked="OnClearClicked" BackgroundColor="#3E3E3E" TextColor="White" FontSize="13"/>
</HorizontalStackLayout>
<Border Grid.Row="1" Stroke="#333" StrokeThickness="1" Background="#000" Padding="8">
<ScrollView x:Name="OutputScroll">
<Label x:Name="OutputLabel" Text="Shell output will appear here…"
TextColor="#00FF40" FontFamily="Monospace" FontSize="12"/>
</ScrollView>
</Border>
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" ColumnSpacing="6" Margin="0,6,0,0">
<Entry x:Name="CmdEntry" Placeholder="Enter shell command…"
Text="ls /" TextColor="White" PlaceholderColor="Gray"
Completed="OnCmdCompleted"/>
<Button Grid.Column="1" Text="▶ Run" Clicked="OnRunClicked"
BackgroundColor="#0078D4" TextColor="White"/>
</Grid>
</Grid>
</ContentPage>
+76
View File
@@ -0,0 +1,76 @@
#nullable enable
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
namespace PS5UploadMobile.Pages;
public partial class ShellPage : ContentPage
{
private readonly StringBuilder _buffer = new();
private bool _shellOpen;
public ShellPage()
{
InitializeComponent();
}
private void AppendLine(string s)
{
_buffer.AppendLine(s);
OutputLabel.Text = _buffer.ToString();
MainThread.BeginInvokeOnMainThread(async () =>
{
await Task.Delay(50);
await OutputScroll.ScrollToAsync(0, double.MaxValue, false);
});
}
private async void OnOpenClicked(object sender, EventArgs e)
{
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
bool ok = await proto.OpenShellAsync();
_shellOpen = ok;
AppendLine(ok ? "🟢 Shell opened" : "🔴 Failed to open shell");
}
private async void OnCloseClicked(object sender, EventArgs e)
{
var proto = PageHelper.Conn.Protocol;
if (proto == null) { AppendLine("(no connection)"); return; }
bool ok = await proto.CloseShellAsync();
_shellOpen = false;
AppendLine(ok ? "🟡 Shell closed" : "⚠️ Close failed");
}
private void OnClearClicked(object sender, EventArgs e)
{
_buffer.Clear();
OutputLabel.Text = "";
}
private async void OnCmdCompleted(object sender, EventArgs e) => await RunCmdAsync();
private async void OnRunClicked(object sender, EventArgs e) => await RunCmdAsync();
private async Task RunCmdAsync()
{
string cmd = CmdEntry.Text?.Trim() ?? "";
if (string.IsNullOrEmpty(cmd)) return;
var proto = await PageHelper.EnsureConnectedAsync(this);
if (proto == null) return;
if (!_shellOpen)
{
bool opened = await proto.OpenShellAsync();
_shellOpen = opened;
if (!opened) { AppendLine("🔴 Shell not open"); return; }
}
AppendLine($"$ {cmd}");
string result = await proto.ExecuteShellCommandAsync(cmd);
if (!string.IsNullOrEmpty(result)) AppendLine(result.TrimEnd('\n', '\r'));
}
}
+79
View File
@@ -0,0 +1,79 @@
#nullable enable
using System;
using System.Threading.Tasks;
using PS5Upload;
namespace PS5UploadMobile.Services
{
public class ConnectionManager
{
private static ConnectionManager? _instance;
private static readonly object _lock = new object();
public string IpAddress { get; set; } = "192.168.0.160";
public int Port { get; set; } = 9113;
public MainPage? MainPageReference { get; set; }
// Shared protocol instance used by all pages
private PS5Protocol? _protocol;
public PS5Protocol? Protocol => _protocol;
public bool IsConnected => _protocol?.IsConnected ?? false;
public event Action? ConnectionStateChanged;
private ConnectionManager() { }
public static ConnectionManager Instance
{
get
{
if (_instance == null)
{
lock (_lock) { if (_instance == null) _instance = new ConnectionManager(); }
}
return _instance;
}
}
public void SetConnection(string ipAddress, int port)
{
IpAddress = ipAddress;
Port = port;
}
public void SetMainPage(MainPage mainPage) => MainPageReference = mainPage;
public async Task<bool> ConnectAsync(string? ip = null, int? port = null)
{
if (!string.IsNullOrWhiteSpace(ip)) IpAddress = ip!;
if (port.HasValue) Port = port.Value;
try { _protocol?.Dispose(); } catch { }
_protocol = new PS5Protocol();
bool ok = await _protocol.ConnectAsync(IpAddress, Port);
if (!ok)
{
try { _protocol.Dispose(); } catch { }
_protocol = null;
}
ConnectionStateChanged?.Invoke();
return ok;
}
public void Disconnect()
{
try { _protocol?.Dispose(); } catch { }
_protocol = null;
ConnectionStateChanged?.Invoke();
}
/// <summary>
/// Ensure the shared protocol is connected, attempt reconnect if not.
/// </summary>
public async Task<PS5Protocol?> EnsureConnectedAsync()
{
if (_protocol != null && _protocol.IsConnected) return _protocol;
return await ConnectAsync() ? _protocol : null;
}
}
}
+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
+461
View File
@@ -0,0 +1,461 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
namespace PS5Upload
{
// ========== Additional commands: Hardware, Games, Saves, Screenshots, Mount, Shell, etc. ==========
public partial class PS5Protocol
{
// ---------------- Hardware info ----------------
public async Task<PS5HardwareInfo?> GetHardwareInfoAsync()
{
try
{
var (response, data) = await RunCommandAsync(Command.GetHwInfo);
if (response != Response.Data) return null;
string text = Encoding.UTF8.GetString(data);
var info = new PS5HardwareInfo();
foreach (var line in text.Split('\n'))
{
var parts = line.Split('=', 2);
if (parts.Length != 2) continue;
var k = parts[0].Trim(); var v = parts[1].Trim();
switch (k)
{
case "model": info.Model = v; break;
case "serial": info.Serial = v; break;
case "has_wlan_bt": info.HasWlanBt = v == "1"; break;
case "has_optical_out": info.HasOpticalOut = v == "1"; break;
case "hw_machine": info.HwMachine = v; break;
case "os": info.OsVersion = v; break;
case "ncpu": if (int.TryParse(v, out var n)) info.NumCpu = n; break;
case "physmem": if (ulong.TryParse(v, out var pm)) info.PhysMem = pm; break;
}
}
return info;
}
catch { return null; }
}
// ---------------- Live sensors ----------------
public async Task<PS5TemperatureInfo?> GetTemperatureInfoAsync()
{
try
{
var (response, data) = await RunCommandAsync(Command.GetTemps);
if (response != Response.Data) return null;
string text = Encoding.UTF8.GetString(data);
var info = new PS5TemperatureInfo();
foreach (var line in text.Split('\n'))
{
var parts = line.Split('=', 2);
if (parts.Length != 2) continue;
var k = parts[0].Trim(); var v = parts[1].Trim();
switch (k)
{
case "cpu_temp": if (int.TryParse(v, out var ct)) info.CpuTemp = ct; break;
case "soc_temp": if (int.TryParse(v, out var st)) info.SocTemp = st; break;
case "cpu_freq_mhz": if (long.TryParse(v, out var cf)) info.CpuFreqMhz = cf; break;
case "soc_power_mw": if (uint.TryParse(v, out var pw)) info.SocPowerMw = pw; break;
}
}
return info;
}
catch { return null; }
}
// ---------------- Power ----------------
public async Task<PS5PowerInfo?> GetPowerInfoAsync()
{
try
{
var (response, data) = await RunCommandAsync(Command.GetPowerInfo);
if (response != Response.Data) return null;
string text = Encoding.UTF8.GetString(data);
var info = new PS5PowerInfo();
foreach (var line in text.Split('\n'))
{
var parts = line.Split('=', 2);
if (parts.Length != 2) continue;
var k = parts[0].Trim(); var v = parts[1].Trim();
switch (k)
{
case "operating_time_sec": if (ulong.TryParse(v, out var a)) info.OperatingTimeSec = a; break;
case "operating_time_hours": if (ulong.TryParse(v, out var b)) info.OperatingTimeHours = b; break;
case "operating_time_minutes": if (ulong.TryParse(v, out var c)) info.OperatingTimeMinutes = c; break;
case "boot_count": if (uint.TryParse(v, out var d)) info.BootCount = d; break;
case "power_consumption_mw": if (uint.TryParse(v, out var e)) info.PowerConsumptionMw = e; break;
}
}
return info;
}
catch { return null; }
}
// ---------------- Mounted games ----------------
public async Task<List<PS5MountedGame>> GetGameListAsync()
{
var result = new List<PS5MountedGame>();
try
{
var (response, data) = await RunCommandAsync(Command.GetGameList);
if (response != Response.Data) return result;
string text = Encoding.UTF8.GetString(data);
if (text.Trim() == "NO_GAMES") return result;
foreach (var line in text.Split('\n'))
{
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split('|');
if (parts.Length < 6) continue;
var g = new PS5MountedGame
{
TitleId = parts[0],
Name = parts[1],
Path = parts[2],
Region = parts[4],
IsActive = parts[5] == "1"
};
if (ulong.TryParse(parts[3], out var sz)) g.Size = sz;
result.Add(g);
}
}
catch { }
return result;
}
public async Task<(bool success, string message)> UnmountGameAsync(string titleId)
{
try
{
var data = Encoding.UTF8.GetBytes(titleId + "\0");
var (response, respData) = await RunCommandAsync(Command.UnmountGame, data);
return (response == Response.Ok, Encoding.UTF8.GetString(respData));
}
catch (Exception ex) { return (false, ex.Message); }
}
public async Task<(bool success, string message)> LaunchGameAsync(string titleId)
{
try
{
var data = Encoding.UTF8.GetBytes(titleId + "\0");
var (response, respData) = await RunCommandAsync(Command.LaunchGame, data);
return (response == Response.Ok, Encoding.UTF8.GetString(respData));
}
catch (Exception ex) { return (false, ex.Message); }
}
public async Task<byte[]?> GetGameIconAsync(string titleId)
{
try
{
var data = Encoding.UTF8.GetBytes(titleId + "\0");
var (response, respData) = await RunCommandAsync(Command.GetGameIcon, data);
if (response != Response.Data || respData == null || respData.Length == 0) return null;
return respData;
}
catch { return null; }
}
public async Task<Dictionary<string, string>?> GetGameDetailsAsync(string titleId)
{
try
{
var data = Encoding.UTF8.GetBytes(titleId + "\0");
var (response, respData) = await RunCommandAsync(Command.GetGameDetails, data);
if (response != Response.Data) return null;
var dict = new Dictionary<string, string>();
foreach (var line in Encoding.UTF8.GetString(respData).Split('\n'))
{
var p = line.Split('=', 2);
if (p.Length == 2) dict[p[0].Trim()] = p[1].Trim();
}
return dict;
}
catch { return null; }
}
// ---------------- Saves ----------------
public async Task<List<PS5SaveGame>> ListSavesAsync()
{
var saves = new List<PS5SaveGame>();
try
{
var (response, data) = await RunCommandAsync(Command.ListSaves);
if (response != Response.Data) return saves;
string text = Encoding.UTF8.GetString(data);
if (text.Trim() == "NO_SAVES") return saves;
foreach (var line in text.Split('\n'))
{
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split('|');
if (parts.Length < 5) continue;
var s = new PS5SaveGame
{
TitleId = parts[0],
UserId = parts[1],
SavePath = parts[2]
};
if (ulong.TryParse(parts[3], out var sz)) s.Size = sz;
if (long.TryParse(parts[4], out var mt)) s.ModifiedUnixTime = mt;
saves.Add(s);
}
}
catch { }
return saves;
}
// ---------------- Screenshots ----------------
public async Task<List<PS5Screenshot>> ListScreenshotsAsync()
{
var shots = new List<PS5Screenshot>();
try
{
var (response, data) = await RunCommandAsync(Command.ListScreenshots);
if (response != Response.Data) return shots;
string body = Encoding.UTF8.GetString(data);
if (body.StartsWith("NO_SCREENSHOTS")) return shots;
foreach (var line in body.Split('\n'))
{
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split('|');
if (parts.Length < 4) continue;
long.TryParse(parts[2], out var size);
long.TryParse(parts[3], out var mtime);
shots.Add(new PS5Screenshot
{
FullPath = parts[0],
FileName = parts[1],
Size = size,
ModifiedTime = DateTimeOffset.FromUnixTimeSeconds(mtime).LocalDateTime
});
}
}
catch { }
return shots.OrderByDescending(s => s.ModifiedTime).ToList();
}
public async Task<(bool success, string message)> DeleteScreenshotAsync(string fullPath)
{
try
{
var data = Encoding.UTF8.GetBytes(fullPath + "\0");
var (response, respData) = await RunCommandAsync(Command.DeleteScreenshot, data);
return (response == Response.Ok, Encoding.UTF8.GetString(respData));
}
catch (Exception ex) { return (false, ex.Message); }
}
// ---------------- Mount all games (long-running, progress) ----------------
public async Task<(bool success, string summary)> MountGamesAsync(Action<string>? onProgress = null)
{
await _commandLock.WaitAsync();
try
{
await SendCommandInternalAsync(Command.MountGames);
// Server streams Progress responses, then final OK or Error
while (true)
{
var (resp, data) = await ReceiveResponseInternalAsync();
if (resp == Response.Progress)
{
onProgress?.Invoke(Encoding.UTF8.GetString(data).TrimEnd('\0'));
}
else if (resp == Response.Ok)
{
return (true, Encoding.UTF8.GetString(data));
}
else if (resp == Response.Error)
{
return (false, Encoding.UTF8.GetString(data));
}
else { return (false, $"Unexpected response {resp}"); }
}
}
catch (Exception ex) { return (false, ex.Message); }
finally { _commandLock.Release(); }
}
// ---------------- Launch Browser ----------------
public async Task<(bool success, string message)> LaunchBrowserAsync(string url)
{
try
{
var data = Encoding.UTF8.GetBytes(url + "\0");
var (response, respData) = await RunCommandAsync(Command.LaunchBrowser, data);
return (response == Response.Ok, Encoding.UTF8.GetString(respData));
}
catch (Exception ex) { return (false, ex.Message); }
}
// Shell methods (OpenShellAsync/ExecuteShellCommandAsync/CloseShellAsync)
// already exist in Protocol.cs — not duplicated here.
}
// ========== Data models ==========
public class PS5HardwareInfo
{
public string Model { get; set; } = "";
public string Serial { get; set; } = "";
public bool HasWlanBt { get; set; }
public bool HasOpticalOut { get; set; }
public string HwMachine { get; set; } = "";
public string OsVersion { get; set; } = "";
public int NumCpu { get; set; }
public ulong PhysMem { get; set; }
public string PhysMemDisplay
{
get
{
if (PhysMem == 0) return "—";
double gb = PhysMem / (1024.0 * 1024.0 * 1024.0);
return $"{gb:F1} GB";
}
}
public string WlanBtDisplay => HasWlanBt ? "✓ Yes" : "✗ None";
public string OpticalDisplay => HasOpticalOut ? "✓ Yes" : "✗ None";
}
public class PS5TemperatureInfo
{
public int CpuTemp { get; set; }
public int SocTemp { get; set; }
public long CpuFreqMhz { get; set; }
public uint SocPowerMw { get; set; }
public string CpuTempDisplay => CpuTemp > 0 ? $"{CpuTemp}°C" : "—";
public string SocTempDisplay => SocTemp > 0 ? $"{SocTemp}°C" : "—";
public string CpuFreqDisplay => CpuFreqMhz > 0 ? $"{CpuFreqMhz / 1000.0:F2} GHz" : "—";
public string SocPowerDisplay => SocPowerMw > 0 ? $"{SocPowerMw / 1000.0:F1} W" : "—";
}
public class PS5PowerInfo
{
public ulong OperatingTimeSec { get; set; }
public ulong OperatingTimeHours { get; set; }
public ulong OperatingTimeMinutes { get; set; }
public uint BootCount { get; set; }
public uint PowerConsumptionMw { get; set; }
}
public class PS5MountedGame : INotifyPropertyChanged
{
public string TitleId { get; set; } = "";
public string Name { get; set; } = "";
public string Path { get; set; } = "";
public ulong Size { get; set; }
public string Region { get; set; } = "";
public bool IsActive { get; set; }
private ImageSource? _icon;
public ImageSource? Icon
{
get => _icon;
set { _icon = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Icon))); }
}
public string SizeDisplay
{
get
{
double mb = Size / (1024.0 * 1024.0);
if (mb < 1024) return $"{mb:F1} MB";
return $"{mb / 1024.0:F2} GB";
}
}
public string StatusDisplay => IsActive ? "✓ Mounted" : "✗ Not Mounted";
public event PropertyChangedEventHandler? PropertyChanged;
}
public class PS5SaveGame : INotifyPropertyChanged
{
public string TitleId { get; set; } = "";
public string UserId { get; set; } = "";
public string SavePath { get; set; } = "";
public ulong Size { get; set; }
public long ModifiedUnixTime { get; set; }
public string SizeDisplay
{
get
{
double mb = Size / (1024.0 * 1024.0);
if (mb < 1) return $"{Size / 1024.0:F1} KB";
if (mb < 1024) return $"{mb:F1} MB";
return $"{mb / 1024.0:F2} GB";
}
}
public string ModifiedDisplay
{
get
{
try
{
var dt = DateTimeOffset.FromUnixTimeSeconds(ModifiedUnixTime).LocalDateTime;
return dt.ToString("yyyy-MM-dd HH:mm");
}
catch { return "—"; }
}
}
public string ShortUserId => UserId.Length > 8 ? UserId.Substring(0, 8) + "…" : UserId;
private string _gameName = "";
public string GameName
{
get => _gameName;
set { _gameName = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(GameName))); }
}
private ImageSource? _icon;
public ImageSource? Icon
{
get => _icon;
set { _icon = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Icon))); }
}
public event PropertyChangedEventHandler? PropertyChanged;
}
public class PS5Screenshot : INotifyPropertyChanged
{
public string FullPath { get; set; } = "";
public string FileName { get; set; } = "";
public long Size { get; set; }
public DateTime ModifiedTime { get; set; }
private ImageSource? _thumbnail;
public ImageSource? Thumbnail
{
get => _thumbnail;
set { _thumbnail = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Thumbnail))); }
}
public event PropertyChangedEventHandler? PropertyChanged;
public string SizeDisplay
{
get
{
if (Size < 1024) return $"{Size} B";
if (Size < 1024 * 1024) return $"{Size / 1024.0:0.0} KB";
return $"{Size / (1024.0 * 1024.0):0.0} MB";
}
}
public string DateDisplay => ModifiedTime.ToString("yyyy-MM-dd HH:mm");
}
}
+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
+5 -1
View File
@@ -8,7 +8,11 @@ echo ""
echo "[+] Compiling..."
rm -f ps5_upload_server.elf
/home/hackman/ps5sdk_copy/bin/prospero-clang -Wall -O3 -pthread -o ps5_upload_server.elf main.c
/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 ""
+3655 -128
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
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.
+179
View File
@@ -0,0 +1,179 @@
# 🚀 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` | 147 MB | Windows GUI client (self-contained, no .NET install required) |
| `PS5UploadMobile.apk` | 30 MB | **NEW:** Android client with full feature parity (8+ tabs) |
| `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.
**Install the Android app:** download `PS5UploadMobile.apk` and install it on your Android device (enable "Install from unknown sources").
---
## 🆕 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)
---
---
## 📱 NEW: Android Mobile Client — Full Feature Parity
The Android app (`PS5UploadMobile.apk`) now has **every feature** of the desktop client, organized via a **hamburger flyout menu (☰)** with a clean light theme for maximum readability:
### 7 Fully-Functional Tabs
- 📂 **Files** — Browse, upload, download, delete, rename, copy/move with drag-friendly UI
- 🎮 **Games** — Mounted games list with cover icons, **Launch Game**, Unmount, Game Details
- 💾 **Saves** — Per-game save browsing with icons, metadata, user IDs
- 📷 **Screenshots** — 2-column gallery with live thumbnails, batch download, clean delete
- 🖥️ **Hardware** — System Info + Live Sensors (CPU/SoC temps, freq, power) + Power Stats + auto-refresh
- 💻 **Shell** — Terminal-style remote command execution with auto-scroll
- 🎮 **Mount Games** — Dedicated page for multi-path scan with streaming progress log
### Mobile-Specific Features
- ✅ **Shared connection** — Connect once in Files tab, all other tabs use it automatically
- ✅ **Thumbnail caching** — Local cache in app cache directory, zero re-downloads
- ✅ **Pull-to-refresh** on games, saves, screenshots
- ✅ **Light flyout menu** — white background, dark text, blue accent on selected item
- ✅ **Dark content pages** — modern readable UI throughout
- ✅ **Interlocked guards** — prevents overlapping refreshes on Hardware tab
- ✅ **Auto-stop timers** — auto-refresh stops on disconnect
- ✅ **0 warnings, 0 errors** build
### Mobile Quick Start
1. Enable **"Install from unknown sources"** in Android settings
2. Download and install `PS5UploadMobile.apk`
3. Load the PS5 payload (`ps5_upload_server.elf`)
4. Open the app, enter your PS5 IP, tap **Connect**
5. Open the **☰ menu** to access all 7 tabs
---
## 🚀 Quick Start (Desktop)
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.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB