🎯 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>
382 lines
8.6 KiB
Markdown
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))%")
|
|
```
|
|
|