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>
This commit is contained in:
2026-02-04 16:20:28 +01:00
parent b474182dd9
commit 83e25e3bd6
18 changed files with 5449 additions and 32 deletions

View File

@@ -28,8 +28,8 @@ async function getRenderedHTML(url, waitFor = 3000) {
// Navigate to the URL and wait for network to be idle
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 30000
waitUntil: 'domcontentloaded',
timeout: 45000
});
// Additional wait to ensure JavaScript content is loaded
@@ -63,12 +63,12 @@ export async function getMangaChapters(mangaSlug) {
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 30000
waitUntil: 'domcontentloaded',
timeout: 45000
});
// Wait for content to load
await page.waitForTimeout(3000);
await new Promise(resolve => setTimeout(resolve, 3000));
// Extract chapters using page.evaluate
const chapters = await page.evaluate(() => {
@@ -136,12 +136,12 @@ export async function getChapterImages(chapterSlug) {
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 30000
waitUntil: 'domcontentloaded',
timeout: 45000
});
// Wait for images to load
await page.waitForTimeout(3000);
await new Promise(resolve => setTimeout(resolve, 3000));
// Extract image URLs
const images = await page.evaluate(() => {
@@ -214,11 +214,11 @@ export async function getMangaInfo(mangaSlug) {
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 30000
waitUntil: 'domcontentloaded',
timeout: 45000
});
await page.waitForTimeout(2000);
await new Promise(resolve => setTimeout(resolve, 2000));
// Extract manga information
const mangaInfo = await page.evaluate(() => {
@@ -315,11 +315,11 @@ export async function getPopularMangas() {
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 30000
waitUntil: 'domcontentloaded',
timeout: 45000
});
await page.waitForTimeout(3000);
await new Promise(resolve => setTimeout(resolve, 3000));
// Extract manga list
const mangas = await page.evaluate(() => {