🎯 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>
8.6 KiB
8.6 KiB
VPS Integration - Code Changes Reference
File: VPSAPIClient.swift (NEW FILE)
Created a complete API client with these sections:
-
Configuration & Initialization
- Singleton pattern
- URLSession setup with appropriate timeouts
- Base URL configuration
-
Health Check
func checkHealth() async throws -> Bool -
Download Operations
func downloadChapter(...) async throws -> VPSDownloadResult- Progress tracking via
@Publishedproperties - Active download tracking
- Progress tracking via
-
Status Checking
func getChapterManifest(...) async throws -> VPSChapterManifest? func listDownloadedChapters(...) async throws -> [VPSChapterInfo] -
Image URLs
func getImageURL(...) -> String -
Management
func deleteChapter(...) async throws -> Bool func getStorageStats() async throws -> VPSStorageStats
File: MangaDetailView.swift
Change 1: Import VPS Client
// Added to MangaDetailView struct
@StateObject private var vpsClient = VPSAPIClient.shared
Change 2: Add VPS Download Button to Toolbar
// In toolbar
Button {
viewModel.showingVPSDownloadAll = true
} label: {
Image(systemName: "icloud.and.arrow.down")
}
.disabled(viewModel.chapters.isEmpty)
Change 3: Add VPS Download Alert
.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
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
// 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
// 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
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
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
// 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
// Added to ReaderView struct
@ObservedObject private var vpsClient = VPSAPIClient.shared
Change 2: Update PageView for VPS Support
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
// 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
// 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
struct VPSDownloadResult {
let success: Bool
let alreadyDownloaded: Bool
let manifest: VPSChapterManifest?
let downloaded: Int?
let failed: Int?
}
VPSChapterManifest
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
struct VPSChapterInfo: Codable {
let chapterNumber: Int
let downloadDate: String
let totalPages: Int
let downloadedPages: Int
let totalSize: Int
let totalSizeMB: String
}
VPSStorageStats
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
- Local Device Storage (fastest, offline)
- VPS Storage (fast, online)
- Original URL (slowest, may fail)
This ensures best performance and reliability.
Error Handling Pattern
All VPS operations follow this pattern:
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:
// Track active downloads
vpsClient.activeDownloads.contains("downloadId")
// Get progress
vpsClient.downloadProgress["downloadId"]
// Display in UI
ProgressView(value: progress)
Text("\(Int(progress * 100))%")