Files
MangaReader/ios-app/Sources/Services/DOWNLOAD_SYSTEM_README.md
renato97 b474182dd9 Initial commit: MangaReader iOS App
 Features:
- App iOS completa para leer manga sin publicidad
- Scraper con WKWebView para manhwaweb.com
- Sistema de descargas offline
- Lector con zoom y navegación
- Favoritos y progreso de lectura
- Compatible con iOS 15+ y Sideloadly/3uTools

📦 Contenido:
- Backend Node.js con Puppeteer (opcional)
- App iOS con SwiftUI
- Scraper de capítulos e imágenes
- Sistema de almacenamiento local
- Testing completo
- Documentación exhaustiva

🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente
  - 21 páginas descargadas
  - 4.68 MB total
  - URLs verificadas y funcionales

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-04 15:34:18 +01:00

9.1 KiB

Sistema de Descarga de Capítulos - MangaReader iOS

Overview

El sistema de descarga de capítulos permite a los usuarios descargar capítulos completos de manga para lectura offline. El sistema está diseñado con arquitectura asíncrona moderna usando Swift async/await.

Componentes Principales

1. DownloadManager (/Sources/Services/DownloadManager.swift)

Gerente centralizado que maneja todas las operaciones de descarga.

Características:

  • Descarga asíncrona de imágenes con concurrencia controlada
  • Máximo 3 descargas simultáneas de capítulos
  • Máximo 5 imágenes simultáneas por capítulo
  • Cancelación de descargas individuales o masivas
  • Seguimiento de progreso en tiempo real
  • Manejo robusto de errores
  • Historial de descargas completadas y fallidas

Uso básico:

let downloadManager = DownloadManager.shared

// Descargar un capítulo
try await downloadManager.downloadChapter(
    mangaSlug: "one-piece",
    mangaTitle: "One Piece",
    chapter: chapter
)

// Descargar múltiples capítulos
await downloadManager.downloadChapters(
    mangaSlug: "one-piece",
    mangaTitle: "One Piece",
    chapters: chapters
)

// Cancelar descarga
downloadManager.cancelDownload(taskId: "taskId")

// Cancelar todas
downloadManager.cancelAllDownloads()

2. MangaDetailView (/Sources/Views/MangaDetailView.swift)

Vista de detalles del manga con funcionalidad de descarga integrada.

Características añadidas:

  • Botón de descarga en la toolbar
  • Descarga individual por capítulo
  • Progreso de descarga visible en cada fila de capítulo
  • Notificaciones de completado/error
  • Alert para descargar últimos 10 o todos los capítulos

Flujo de descarga:

  1. Usuario toca botón de descarga en toolbar → muestra alert
  2. Selecciona cantidad de capítulos a descargar
  3. Cada capítulo muestra progreso de descarga en tiempo real
  4. Notificación aparece al completar todas las descargas
  5. Capítulos descargados muestran checkmark verde

3. DownloadsView (/Sources/Views/DownloadsView.swift)

Vista dedicada para gestionar todas las descargas.

Tabs:

  • Activas: Descargas en progreso con barra de progreso
  • Completadas: Historial de descargas exitosas
  • Fallidas: Descargas con errores, permite reintentar

Funcionalidades:

  • Cancelar descargas individuales
  • Cancelar todas las descargas activas
  • Limpiar historiales (completadas/fallidas)
  • Ver tamaño de almacenamiento usado
  • Limpiar todo el almacenamiento descargado

4. StorageService (/Sources/Services/StorageService.swift)

Servicio de almacenamiento ya existente, ahora con soporte para descargas.

Métodos utilizados:

// Guardar imagen descargada
try await storage.saveImage(
    image,
    mangaSlug: "manga-slug",
    chapterNumber: 1,
    pageIndex: 0
)

// Verificar si capítulo está descargado
storage.isChapterDownloaded(mangaSlug: "manga-slug", chapterNumber: 1)

// Obtener directorio del capítulo
let chapterDir = storage.getChapterDirectory(
    mangaSlug: "manga-slug",
    chapterNumber: 1
)

// Obtener URL de imagen local
if let imageURL = storage.getImageURL(
    mangaSlug: "manga-slug",
    chapterNumber: 1,
    pageIndex: 0
) {
    // Usar imagen local
}

// Eliminar capítulo descargado
storage.deleteDownloadedChapter(
    mangaSlug: "manga-slug",
    chapterNumber: 1
)

// Obtener tamaño de almacenamiento
let size = storage.getStorageSize()
let formatted = storage.formatFileSize(size)

Modelos de Datos

DownloadTask

Representa una tarea de descarga individual:

class DownloadTask: ObservableObject {
    let id: String
    let mangaSlug: String
    let mangaTitle: String
    let chapterNumber: Int
    let imageURLs: [String]

    @Published var state: DownloadState
    @Published var downloadedPages: Int
    @Published var progress: Double
}

DownloadState

Estados posibles de una descarga:

enum DownloadState {
    case pending
    case downloading(progress: Double)
    case completed
    case failed(error: String)
    case cancelled
}

DownloadError

Tipos de errores de descarga:

enum DownloadError: LocalizedError {
    case alreadyDownloaded
    case noImagesFound
    case invalidURL
    case invalidResponse
    case httpError(statusCode: Int)
    case invalidImageData
    case cancelled
    case storageError(String)
}

Configuración

Parámetros de Descarga

En DownloadManager:

private let maxConcurrentDownloads = 3           // Máximo de capítulos simultáneos
private let maxConcurrentImagesPerChapter = 5    // Máximo de imágenes simultáneas por capítulo

Calidad de Imagen

En StorageService.saveImage():

image.jpegData(compressionQuality: 0.8)  // 80% de calidad JPEG

En DownloadExtensions:

func optimizedForStorage() -> Data? {
    // Redimensiona si > 2048px
    // Comprime a 75% de calidad
}

Integración con ReaderView

Para leer capítulos descargados:

struct ReaderView: View {
    let chapter: Chapter
    let mangaSlug: String

    @StateObject private var storage = StorageService.shared

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(pageIndices, id: \.self) { index in
                    if let imageURL = storage.getImageURL(
                        mangaSlug: mangaSlug,
                        chapterNumber: chapter.number,
                        pageIndex: index
                    ) {
                        // Usar imagen local
                        AsyncImage(url: imageURL) { image in
                            image.resizable()
                        } placeholder: {
                            ProgressView()
                        }
                    } else {
                        // Fallback a URL remota
                        RemoteChapterPage(url: remoteURL)
                    }
                }
            }
        }
    }
}

Notificaciones

El sistema emite notificaciones para seguimiento:

extension Notification.Name {
    static let downloadDidStart = Notification.Name("downloadDidStart")
    static let downloadDidUpdate = Notification.Name("downloadDidUpdate")
    static let downloadDidComplete = Notification.Name("downloadDidComplete")
    static let downloadDidFail = Notification.Name("downloadDidFail")
    static let downloadDidCancel = Notification.Name("downloadDidCancel")
}

Manejo de Errores

Errores de Red

  • Timeout: 30 segundos por imagen
  • Reintentos: Manejados por URLSession
  • HTTP errors: Capturados y reportados en UI

Errores de Almacenamiento

  • Espacio insuficiente: Error con mensaje descriptivo
  • Permisos: Manejados por FileManager
  • Corrupción de archivos: Archivos eliminados y descarga reiniciada

Errores de Scraping

  • No se encontraron imágenes: Error noImagesFound
  • Página no carga: Error del scraper propagado
  • Cambios en la web: Requieren actualización del scraper

Best Practices

1. Concurrencia

El sistema usa Swift Concurrency:

  • async/await para operaciones asíncronas
  • Task para crear contextos de concurrencia
  • @MainActor para actualizaciones de UI
  • TaskGroup para descargas en paralelo

2. Memoria

  • Imágenes comprimidas antes de guardar
  • Descarga limitada a 5 imágenes simultáneas
  • Limpieza automática de historiales (50 completadas, 20 fallidas)

3. UX

  • Progreso visible en tiempo real
  • Cancelación en cualquier punto
  • Notificaciones de estado
  • Estados vacíos descriptivos
  • Feedback inmediato de acciones

4. Robustez

  • Validación de estados antes de descargar
  • Limpieza de archivos parciales al cancelar
  • Verificación de archivos existentes
  • Manejo exhaustivo de errores

Testing

Pruebas Unitarias

func testDownloadManager() async throws {
    let manager = DownloadManager.shared

    // Probar descarga individual
    try await manager.downloadChapter(
        mangaSlug: "test",
        mangaTitle: "Test Manga",
        chapter: testChapter
    )

    XCTAssertTrue(manager.activeDownloads.isEmpty)
    XCTAssertEqual(manager.completedDownloads.count, 1)
}

Pruebas de Integración

  • Descargar capítulo completo
  • Cancelar descarga a mitad
  • Descargar múltiples capítulos
  • Probar con y sin conexión
  • Verificar persistencia de archivos

Troubleshooting

Descargas no inician

  • Verificar conexión a internet
  • Verificar que el scraper puede acceder a la web
  • Revisar logs del scraper

Progreso no actualiza

  • Asegurar que las vistas están en @MainActor
  • Verificar que DownloadTask es @ObservedObject
  • Chequear que las propiedades son @Published

Archivos no se guardan

  • Verificar permisos de la app
  • Chequear espacio disponible
  • Revisar que directorios existen

Imágenes corruptas

  • Verificar calidad de compresión
  • Chequear que URLs sean válidas
  • Probar redimensionado de imágenes

Futuras Mejoras

  • Soporte para reanudar descargas pausadas
  • Priorización de descargas
  • Descarga automática de nuevos capítulos
  • Compresión adicional de imágenes
  • Soporte para formatos WebP
  • Batch operations en StorageService
  • Background downloads con URLSession
  • Metrics y analytics de descargas