Files
MangaReader/CHANGES.md
renato97 83e25e3bd6 feat: Add VPS storage system and complete integration
🎯 Overview:
Implemented complete VPS-based storage system allowing the iOS app to download
and store manga chapters on the VPS for ad-free offline reading.

📦 Backend Changes:
- Added storage.js service for managing chapter downloads (270 lines)
- Updated server.js with 6 new storage endpoints:
  - POST /api/download - Download chapters to VPS
  - GET /api/storage/chapters/:mangaSlug - List downloaded chapters
  - GET /api/storage/chapter/:mangaSlug/:chapterNumber - Check download status
  - GET /api/storage/image/:mangaSlug/:chapterNumber/:pageIndex - Serve images
  - DELETE /api/storage/chapter/:mangaSlug/:chapterNumber - Delete chapters
  - GET /api/storage/stats - Get storage statistics
- Fixed scraper.js Puppeteer compatibility issues (waitForTimeout, networkidle0)
- Added comprehensive test suite:
  - test-vps-flow.js (13 tests - 100% pass rate)
  - test-concurrent-downloads.js (10 tests for parallel operations)
  - run-tests.sh automation script

📱 iOS App Changes:
- Created APIConfig.swift with VPS connection settings
- Created VPSAPIClient.swift service (727 lines) for backend communication
- Updated MangaDetailView.swift with VPS download integration:
  - Cloud icon for VPS-available chapters
  - Upload button to download chapters to VPS
  - Progress indicators for active downloads
  - Bulk download options (last 10 or all chapters)
- Updated ReaderView.swift to load images from VPS first
- Progressive enhancement: app works without VPS, enhances when available

 Tests:
- All 13 VPS flow tests passing (100%)
- Tests verify: scraping, downloading, storage, serving, deletion, stats
- Chapter 789 download test: 21 images, 4.68 MB
- Concurrent download tests verify no race conditions

🔧 Configuration:
- VPS URL: https://gitea.cbcren.online:3001
- Storage location: /home/ren/ios/MangaReader/storage/
- Static file serving: /storage path

📚 Documentation:
- Added VPS_INTEGRATION_SUMMARY.md - Complete feature overview
- Added CHANGES.md - Detailed code changes reference
- Added TEST_README.md, TEST_QUICK_START.md, TEST_SUMMARY.md
- Added APIConfig README with usage examples

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-04 16:20:28 +01:00

382 lines
8.6 KiB
Markdown

# VPS Integration - Code Changes Reference
## File: VPSAPIClient.swift (NEW FILE)
Created a complete API client with these sections:
1. **Configuration & Initialization**
- Singleton pattern
- URLSession setup with appropriate timeouts
- Base URL configuration
2. **Health Check**
```swift
func checkHealth() async throws -> Bool
```
3. **Download Operations**
```swift
func downloadChapter(...) async throws -> VPSDownloadResult
```
- Progress tracking via `@Published` properties
- Active download tracking
4. **Status Checking**
```swift
func getChapterManifest(...) async throws -> VPSChapterManifest?
func listDownloadedChapters(...) async throws -> [VPSChapterInfo]
```
5. **Image URLs**
```swift
func getImageURL(...) -> String
```
6. **Management**
```swift
func deleteChapter(...) async throws -> Bool
func getStorageStats() async throws -> VPSStorageStats
```
---
## File: MangaDetailView.swift
### Change 1: Import VPS Client
```swift
// Added to MangaDetailView struct
@StateObject private var vpsClient = VPSAPIClient.shared
```
### Change 2: Add VPS Download Button to Toolbar
```swift
// In toolbar
Button {
viewModel.showingVPSDownloadAll = true
} label: {
Image(systemName: "icloud.and.arrow.down")
}
.disabled(viewModel.chapters.isEmpty)
```
### Change 3: Add VPS Download Alert
```swift
.alert("Descargar a VPS", isPresented: $viewModel.showingVPSDownloadAll) {
Button("Cancelar", role: .cancel) { }
Button("Últimos 10 a VPS") {
Task {
await viewModel.downloadLastChaptersToVPS(count: 10)
}
}
Button("Todos a VPS") {
Task {
await viewModel.downloadAllChaptersToVPS()
}
}
} message: {
Text("¿Cuántos capítulos quieres descargar al servidor VPS?")
}
```
### Change 4: Update ChapterRowView
```swift
struct ChapterRowView: View {
// Added:
let onVPSDownloadToggle: () async -> Void
@ObservedObject var vpsClient = VPSAPIClient.shared
@State private var isVPSDownloaded = false
@State private var isVPSChecked = false
```
### Change 5: Add VPS Status Indicator in ChapterRowView Body
```swift
// VPS Download Button / Status
if isVPSChecked {
if isVPSDownloaded {
Image(systemName: "icloud.fill")
.foregroundColor(.blue)
} else {
Button {
Task {
await onVPSDownloadToggle()
}
} label: {
Image(systemName: "icloud.and.arrow.up")
.foregroundColor(.blue)
}
.buttonStyle(.plain)
}
}
```
### Change 6: Add VPS Progress Display
```swift
// Mostrar progreso de descarga VPS
if vpsClient.activeDownloads.contains("\(mangaSlug)-\(chapter.number)"),
let progress = vpsClient.downloadProgress["\(mangaSlug)-\(chapter.number)"] {
HStack {
Image(systemName: "icloud.and.arrow.down")
.font(.caption2)
.foregroundColor(.blue)
ProgressView(value: progress)
.progressViewStyle(.linear)
.frame(maxWidth: 100)
Text("VPS \(Int(progress * 100))%")
.font(.caption2)
.foregroundColor(.blue)
}
}
```
### Change 7: Add VPS Status Check Function
```swift
private func checkVPSStatus() async {
do {
let manifest = try await vpsClient.getChapterManifest(
mangaSlug: mangaSlug,
chapterNumber: chapter.number
)
isVPSDownloaded = manifest != nil
isVPSChecked = true
} catch {
isVPSDownloaded = false
isVPSChecked = true
}
}
```
### Change 8: Update chaptersList to Pass VPS Callback
```swift
ChapterRowView(
chapter: chapter,
mangaSlug: manga.slug,
onTap: {
viewModel.selectedChapter = chapter
},
onDownloadToggle: {
await viewModel.downloadChapter(chapter)
},
onVPSDownloadToggle: { // NEW
await viewModel.downloadChapterToVPS(chapter)
}
)
```
### Change 9: ViewModel Additions
```swift
// New Published Property
@Published var showingVPSDownloadAll = false
// New Dependency
private let vpsClient = VPSAPIClient.shared
// New Methods
func downloadChapterToVPS(_ chapter: Chapter) async {
do {
let imageUrls = try await scraper.scrapeChapterImages(chapterSlug: chapter.slug)
let result = try await vpsClient.downloadChapter(
mangaSlug: manga.slug,
chapterNumber: chapter.number,
chapterSlug: chapter.slug,
imageUrls: imageUrls
)
// Handle result and show notification
} catch {
// Handle error
}
}
func downloadAllChaptersToVPS() async { /* ... */ }
func downloadLastChaptersToVPS(count: Int) async { /* ... */ }
```
---
## File: ReaderView.swift
### Change 1: Add VPS Client
```swift
// Added to ReaderView struct
@ObservedObject private var vpsClient = VPSAPIClient.shared
```
### Change 2: Update PageView for VPS Support
```swift
struct PageView: View {
// Added:
@ObservedObject var vpsClient = VPSAPIClient.shared
@State private var useVPS = false
var body: some View {
// ...
if let localURL = StorageService.shared.getImageURL(...) {
// Load from local cache
} else if useVPS {
// Load from VPS
let vpsImageURL = vpsClient.getImageURL(
mangaSlug: mangaSlug,
chapterNumber: chapterNumber,
pageIndex: page.index + 1
)
AsyncImage(url: URL(string: vpsImageURL)) { /* ... */ }
} else {
// Load from original URL
fallbackImage
}
// ...
.task {
// Check if VPS has this chapter
if let manifest = try? await vpsClient.getChapterManifest(...) {
useVPS = true
}
}
}
private var fallbackImage: some View { /* ... */ }
}
```
### Change 3: Update ReaderViewModel
```swift
// New Published Property
@Published var isVPSDownloaded = false
// New Dependency
private let vpsClient = VPSAPIClient.shared
// Updated loadPages()
func loadPages() async {
// 1. Check VPS first
if let vpsManifest = try await vpsClient.getChapterManifest(...) {
// Load from VPS
isVPSDownloaded = true
}
// 2. Then local storage
else if let downloadedChapter = storage.getDownloadedChapter(...) {
// Load from local
isDownloaded = true
}
// 3. Finally scrape
else {
// Scrape from original
}
}
```
### Change 4: Update Reader Footer
```swift
// Page indicator section
if viewModel.isVPSDownloaded {
Label("VPS", systemImage: "icloud.fill")
.font(.caption)
.foregroundColor(.blue)
}
if viewModel.isDownloaded {
Label("Local", systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundColor(.green)
}
```
---
## Data Models (in VPSAPIClient.swift)
### VPSDownloadResult
```swift
struct VPSDownloadResult {
let success: Bool
let alreadyDownloaded: Bool
let manifest: VPSChapterManifest?
let downloaded: Int?
let failed: Int?
}
```
### VPSChapterManifest
```swift
struct VPSChapterManifest: Codable {
let mangaSlug: String
let chapterNumber: Int
let totalPages: Int
let downloadedPages: Int
let failedPages: Int
let downloadDate: String
let totalSize: Int
let images: [VPSImageInfo]
}
```
### VPSChapterInfo
```swift
struct VPSChapterInfo: Codable {
let chapterNumber: Int
let downloadDate: String
let totalPages: Int
let downloadedPages: Int
let totalSize: Int
let totalSizeMB: String
}
```
### VPSStorageStats
```swift
struct VPSStorageStats: Codable {
let totalMangas: Int
let totalChapters: Int
let totalSize: Int
let totalSizeMB: String
let totalSizeFormatted: String
let mangaDetails: [VPSMangaDetail]
}
```
---
## Priority Order for Image Loading
1. **Local Device Storage** (fastest, offline)
2. **VPS Storage** (fast, online)
3. **Original URL** (slowest, may fail)
This ensures best performance and reliability.
---
## Error Handling Pattern
All VPS operations follow this pattern:
```swift
do {
let result = try await vpsClient.someMethod(...)
// Handle success
} catch {
// Handle error - show user notification
notificationMessage = "Error: \(error.localizedDescription)"
showDownloadNotification = true
}
```
---
## Progress Tracking Pattern
For downloads:
```swift
// Track active downloads
vpsClient.activeDownloads.contains("downloadId")
// Get progress
vpsClient.downloadProgress["downloadId"]
// Display in UI
ProgressView(value: progress)
Text("\(Int(progress * 100))%")
```