Files
MangaReader/docs/API.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

22 KiB

API Documentation - MangaReader

Este documento proporciona la documentación completa de la API de MangaReader, incluyendo modelos de datos, servicios, ViewModels y sus responsabilidades.

Tabla de Contenidos


Modelos de Datos

Los modelos de datos se encuentran en ios-app/Sources/Models/Manga.swift. Son estructuras inmutables que conforman a protocolos Swift estándar para serialización e identificación.

Manga

Representa la información completa de un manga.

struct Manga: Codable, Identifiable, Hashable {
    let id: String { slug }
    let slug: String
    let title: String
    let description: String
    let genres: [String]
    let status: String
    let url: String
    let coverImage: String?

    enum CodingKeys: String, CodingKey {
        case slug, title, description, genres, status, url, coverImage
    }

    var displayStatus: String { ... }
}

Propiedades

Propiedad Tipo Descripción Ejemplo
id String Identificador único (computed, igual a slug) "one-piece_1695365223767"
slug String Slug único del manga usado en URLs "one-piece_1695365223767"
title String Título del manga "One Piece"
description String Descripción o sinopsis "La historia de piratas..."
genres [String] Array de géneros literarios ["Acción", "Aventura"]
status String Estado de publicación (crudo) "PUBLICANDOSE"
url String URL completa del manga "https://manhwaweb.com/manga/..."
coverImage String? URL de imagen de portada (opcional) "https://..."

Métodos Computados

displayStatus: String

  • Retorna el estado traducido y formateado para mostrar en UI
  • Mapeos:
    • "PUBLICANDOSE""En publicación"
    • "FINALIZADO""Finalizado"
    • "EN_PAUSA", "EN_ESPERA""En pausa"
    • Otro → retorna valor original

Ejemplo de Uso

let manga = Manga(
    slug: "one-piece_1695365223767",
    title: "One Piece",
    description: "La historia de Monkey D. Luffy...",
    genres: ["Acción", "Aventura", "Comedia"],
    status: "PUBLICANDOSE",
    url: "https://manhwaweb.com/manga/one-piece_1695365223767",
    coverImage: "https://example.com/cover.jpg"
)

print(manga.displayStatus) // "En publicación"
print(manga.id) // "one-piece_1695365223767"

Chapter

Representa un capítulo individual de un manga.

struct Chapter: Codable, Identifiable, Hashable {
    let id: Int { number }
    let number: Int
    let title: String
    let url: String
    let slug: String
    var isRead: Bool = false
    var isDownloaded: Bool = false
    var lastReadPage: Int = 0

    var displayNumber: String { ... }
    var progress: Double { ... }
}

Propiedades

Propiedad Tipo Descripción Ejemplo
id Int Identificador único (computed, igual a number) 1
number Int Número del capítulo 1
title String Título del capítulo "El inicio de la aventura"
url String URL completa del capítulo "https://manhwaweb.com/leer/..."
slug String Slug para identificar el capítulo "one-piece/capitulo-1"
isRead Bool Estado de lectura (mutable) false
isDownloaded Bool Estado de descarga (mutable) false
lastReadPage Int Última página leída (mutable) 5

Métodos Computados

displayNumber: String

  • Retorna string formateado para mostrar
  • Formato: "Capítulo {number}"

progress: Double

  • Retorna progreso como Double para ProgressViews
  • Valor: Double(lastReadPage)

Ejemplo de Uso

var chapter = Chapter(
    number: 1,
    title: "El inicio",
    url: "https://manhwaweb.com/leer/one-piece/1",
    slug: "one-piece/1"
)

chapter.isRead = true
chapter.lastReadPage = 15

print(chapter.displayNumber) // "Capítulo 1"
print(chapter.progress) // 15.0

MangaPage

Representa una página individual (imagen) de un capítulo.

struct MangaPage: Codable, Identifiable, Hashable {
    let id: String { url }
    let url: String
    let index: Int
    var isCached: Bool = false

    var thumbnailURL: String { ... }
}

Propiedades

Propiedad Tipo Descripción Ejemplo
id String Identificador único (computed, igual a url) "https://..."
url String URL completa de la imagen "https://example.com/page1.jpg"
index Int Índice de la página en el capítulo 0
isCached Bool Estado de cache local (mutable) false

Métodos Computados

thumbnailURL: String

  • Actualmente retorna la misma URL
  • Futuro: implementar versión thumbnail optimizada

ReadingProgress

Almacena el progreso de lectura de un usuario.

struct ReadingProgress: Codable {
    let mangaSlug: String
    let chapterNumber: Int
    let pageNumber: Int
    let timestamp: Date

    var isCompleted: Bool { ... }
}

Propiedades

Propiedad Tipo Descripción
mangaSlug String Slug del manga
chapterNumber Int Número del capítulo
pageNumber Int Página actual
timestamp Date Fecha/hora de lectura

Métodos Computados

isCompleted: Bool

  • Retorna true si el usuario leyó más de 5 páginas
  • Lógica: return pageNumber > 5

DownloadedChapter

Representa un capítulo descargado localmente.

struct DownloadedChapter: Codable, Identifiable {
    let id: String { "\(mangaSlug)-chapter\(chapterNumber)" }
    let mangaSlug: String
    let mangaTitle: String
    let chapterNumber: Int
    let pages: [MangaPage]
    let downloadedAt: Date
    var totalSize: Int64 = 0

    var displayTitle: String { ... }
}

Propiedades

Propiedad Tipo Descripción
id String ID compuesto único
mangaSlug String Slug del manga
mangaTitle String Título del manga
chapterNumber Int Número del capítulo
pages [MangaPage] Array de páginas
downloadedAt Date Fecha de descarga
totalSize Int64 Tamaño total en bytes

Servicios

Los servicios encapsulan la lógica de negocio y se encuentran en ios-app/Sources/Services/.

ManhwaWebScraper

Servicio responsable del scraping de contenido web desde manhwaweb.com.

@MainActor
class ManhwaWebScraper: NSObject, ObservableObject

Propiedades

Propiedad Tipo Acceso Descripción
shared ManhwaWebScraper static Instancia singleton compartida
webView WKWebView? private WebView para ejecutar JavaScript

Métodos Públicos

scrapeMangaInfo(mangaSlug:)

Obtiene la información completa de un manga.

Firma:

func scrapeMangaInfo(mangaSlug: String) async throws -> Manga

Parámetros:

  • mangaSlug: String - Slug único del manga

Retorna:

  • Manga - Objeto con información completa del manga

Errors:

  • ScrapingError.webViewNotInitialized
  • ScrapingError.pageLoadFailed
  • ScrapingError.noContentFound

Ejemplo:

do {
    let manga = try await ManhwaWebScraper.shared.scrapeMangaInfo(
        mangaSlug: "one-piece_1695365223767"
    )
    print("Manga: \(manga.title)")
} catch {
    print("Error: \(error.localizedDescription)")
}

Proceso Interno:

  1. Construye URL: https://manhwaweb.com/manga/{slug}
  2. Carga URL en WKWebView
  3. Espera 3 segundos a que JavaScript renderice
  4. Ejecuta JavaScript para extraer:
    • Título (de <h1> o <title>)
    • Descripción (de <p> con >100 caracteres)
    • Géneros (de links /genero/*)
    • Estado (via regex)
    • Cover image (de .cover img)
  5. Parsea resultado a Manga

scrapeChapters(mangaSlug:)

Obtiene la lista de capítulos de un manga.

Firma:

func scrapeChapters(mangaSlug: String) async throws -> [Chapter]

Parámetros:

  • mangaSlug: String - Slug del manga

Retorna:

  • [Chapter] - Array de capítulos ordenados descendente

Errors:

  • ScrapingError.webViewNotInitialized
  • ScrapingError.pageLoadFailed
  • ScrapingError.parsingError

Ejemplo:

do {
    let chapters = try await ManhwaWebScraper.shared.scrapeChapters(
        mangaSlug: "one-piece_1695365223767"
    )
    print("Found \(chapters.count) chapters")
} catch {
    print("Error: \(error.localizedDescription)")
}

Proceso Interno:

  1. Carga página del manga
  2. Ejecuta JavaScript que:
    • Busca todos los links /leer/*
    • Extrae número de capítulo via regex
    • Filtra duplicados
    • Ordena descendente
  3. Parsea a array de Chapter

scrapeChapterImages(chapterSlug:)

Obtiene las URLs de las imágenes de un capítulo.

Firma:

func scrapeChapterImages(chapterSlug: String) async throws -> [String]

Parámetros:

  • chapterSlug: String - Slug del capítulo

Retorna:

  • [String] - Array de URLs de imágenes en orden

Errors:

  • ScrapingError.webViewNotInitialized
  • ScrapingError.pageLoadFailed

Ejemplo:

do {
    let images = try await ManhwaWebScraper.shared.scrapeChapterImages(
        chapterSlug: "one-piece/1"
    )
    print("Found \(images.count) pages")
    for (index, imageUrl) in images.enumerated() {
        print("Page \(index + 1): \(imageUrl)")
    }
} catch {
    print("Error: \(error.localizedDescription)")
}

Proceso Interno:

  1. Carga URL del capítulo
  2. Espera 5 segundos (para imágenes)
  3. Ejecuta JavaScript que:
    • Selecciona todas las etiquetas <img>
    • Filtra elementos de UI (avatars, icons, logos)
    • Elimina duplicados
  4. Retorna array de URLs

Métodos Privados

setupWebView()

Configura el WKWebView con preferencias optimizadas.

loadURLAndWait(_:waitForImages:)

Carga una URL y espera a que JavaScript termine de renderizar.

Parámetros:

  • url: URL - URL a cargar
  • waitForImages: Bool - Si true, espera 5 segundos; si false, 3 segundos

StorageService

Servicio responsable del almacenamiento local de datos.

class StorageService

Propiedades

Propiedad Tipo Acceso Descripción
shared StorageService static Instancia singleton compartida
documentsDirectory URL private Directorio de documentos
chaptersDirectory URL private Directorio de capítulos

Gestión de Favoritos

getFavorites()
func getFavorites() -> [String]

Retorna array de slugs de mangas favoritos.

saveFavorite(mangaSlug:)
func saveFavorite(mangaSlug: String)

Guarda un manga como favorito (no duplica si ya existe).

removeFavorite(mangaSlug:)
func removeFavorite(mangaSlug: String)

Elimina un manga de favoritos.

isFavorite(mangaSlug:)
func isFavorite(mangaSlug: String) -> Bool

Verifica si un manga es favorito.

Ejemplo:

let storage = StorageService.shared

// Guardar favorito
storage.saveFavorite(mangaSlug: "one-piece_1695365223767")

// Verificar
if storage.isFavorite(mangaSlug: "one-piece_1695365223767") {
    print("Es favorito")
}

// Listar todos
let favorites = storage.getFavorites()
print("Tienes \(favorites.count) favoritos")

// Eliminar
storage.removeFavorite(mangaSlug: "one-piece_1695365223767")

Gestión de Progreso de Lectura

saveReadingProgress(_:)
func saveReadingProgress(_ progress: ReadingProgress)

Guarda o actualiza el progreso de lectura.

getReadingProgress(mangaSlug:chapterNumber:)
func getReadingProgress(mangaSlug: String, chapterNumber: Int) -> ReadingProgress?

Retorna el progreso de un capítulo específico.

getAllReadingProgress()
func getAllReadingProgress() -> [ReadingProgress]

Retorna todo el progreso guardado.

getLastReadChapter(mangaSlug:)
func getLastReadChapter(mangaSlug: String) -> ReadingProgress?

Retorna el capítulo más reciente leído de un manga.

Ejemplo:

let storage = StorageService.shared

// Guardar progreso
let progress = ReadingProgress(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1,
    pageNumber: 15,
    timestamp: Date()
)
storage.saveReadingProgress(progress)

// Recuperar progreso
if let savedProgress = storage.getReadingProgress(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1
) {
    print("Última página: \(savedProgress.pageNumber)")
}

// Último capítulo leído
if let lastChapter = storage.getLastReadChapter(mangaSlug: "one-piece_1695365223767") {
    print("Último capítulo: \(lastChapter.chapterNumber)")
}

Gestión de Capítulos Descargados

saveDownloadedChapter(_:)
func saveDownloadedChapter(_ chapter: DownloadedChapter)

Guarda metadata de un capítulo descargado.

getDownloadedChapters()
func getDownloadedChapters() -> [DownloadedChapter]

Retorna todos los capítulos descargados.

isChapterDownloaded(mangaSlug:chapterNumber:)
func isChapterDownloaded(mangaSlug: String, chapterNumber: Int) -> Bool

Verifica si un capítulo está descargado.

deleteDownloadedChapter(mangaSlug:chapterNumber:)
func deleteDownloadedChapter(mangaSlug: String, chapterNumber: Int)

Elimina un capítulo descargado (archivos + metadata).

Ejemplo:

let storage = StorageService.shared

// Verificar si está descargado
if storage.isChapterDownloaded(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1
) {
    print("Capítulo ya descargado")
} else {
    print("Capítulo no descargado")
}

// Eliminar capítulo
storage.deleteDownloadedChapter(
    mangaSlug: "one-piece_1695365223767",
    chapterNumber: 1
)

Gestión de Imágenes

saveImage(_:mangaSlug:chapterNumber:pageIndex:)
func saveImage(_ image: UIImage, mangaSlug: String, chapterNumber: Int, pageIndex: Int) async throws -> URL

Guarda una imagen en disco local.

Parámetros:

  • image: UIImage - Imagen a guardar
  • mangaSlug: String - Slug del manga
  • chapterNumber: Int - Número del capítulo
  • pageIndex: Int - Índice de la página

Retorna:

  • URL - Ruta del archivo guardado

Errors:

  • Error si no se puede convertir o guardar

Ejemplo:

let storage = StorageService.shared

do {
    let imageURL = try await storage.saveImage(
        image: myUIImage,
        mangaSlug: "one-piece_1695365223767",
        chapterNumber: 1,
        pageIndex: 0
    )
    print("Imagen guardada en: \(imageURL.path)")
} catch {
    print("Error guardando imagen: \(error)")
}
loadImage(mangaSlug:chapterNumber:pageIndex:)
func loadImage(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> UIImage?

Carga una imagen desde disco.

getImageURL(mangaSlug:chapterNumber:pageIndex:)
func getImageURL(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> URL?

Retorna la URL local de una imagen si existe.


Gestión de Almacenamiento

getStorageSize()
func getStorageSize() -> Int64

Retorna el tamaño total usado en bytes.

clearAllDownloads()
func clearAllDownloads()

Elimina todos los capítulos descargados.

formatFileSize(_:)
func formatFileSize(_ bytes: Int64) -> String

Formatea bytes a string legible (KB, MB, GB).

Ejemplo:

let storage = StorageService.shared

// Obtener tamaño usado
let size = storage.getStorageSize()
let formatted = storage.formatFileSize(size)
print("Almacenamiento usado: \(formatted)")

// Limpiar todo
storage.clearAllDownloads()

ViewModels

Los ViewModels coordinan entre Services y Views.

MangaListViewModel

ViewModel para la lista principal de mangas.

@MainActor
class MangaListViewModel: ObservableObject

Propiedades Publicadas

Propiedad Tipo Descripción
mangas [Manga] Lista de mangas cargados
isLoading Bool Indicador de carga
searchText String Texto de búsqueda
filter MangaFilter Filtro actual (all/favorites/downloaded)
newMangaSlug String Slug del nuevo manga a agregar

Métodos Públicos

loadMangas()
func loadMangas() async

Carga los mangas guardados en favoritos.

addManga(_:)
func addManga(_ slug: String) async

Agrega un nuevo manga mediante scraping.

Propiedades Computadas

filteredMangas
var filteredMangas: [Manga]

Retorna mangas filtrados por búsqueda y categoría.

Ejemplo:

@StateObject private var viewModel = MangaListViewModel()

// Cargar mangas
await viewModel.loadMangas()

// Agregar manga
await viewModel.addManga("one-piece_1695365223767")

// Filtrar
viewModel.filter = .favorites
viewModel.searchText = "one"

MangaDetailViewModel

ViewModel para el detalle de un manga.

@MainActor
class MangaDetailViewModel: ObservableObject

Propiedades Publicadas

Propiedad Tipo Descripción
chapters [Chapter] Lista de capítulos
isLoadingChapters Bool Indicador de carga
isFavorite Bool Estado de favorito
selectedChapter Chapter? Capítulo seleccionado
showingDownloadAll Bool Mostrar diálogo de descarga

Métodos Públicos

loadChapters()
func loadChapters() async

Carga los capítulos del manga.

toggleFavorite()
func toggleFavorite()

Alterna el estado de favorito.

downloadAllChapters()
func downloadAllChapters()

Inicia descarga de todos los capítulos.

downloadLastChapters(count:)
func downloadLastChapters(count: Int)

Descarga los últimos N capítulos.

Ejemplo:

@StateObject private var viewModel = MangaDetailViewModel(manga: manga)

// Cargar capítulos
await viewModel.loadChapters()

// Marcar favorito
viewModel.toggleFavorite()

// Descargar últimos 10
viewModel.downloadLastChapters(count: 10)

ReaderViewModel

ViewModel para el lector de capítulos.

@MainActor
class ReaderViewModel: ObservableObject

Propiedades Publicadas

Propiedad Tipo Descripción
pages [MangaPage] Lista de páginas
currentPage Int Página actual
isLoading Bool Indicador de carga
showError Bool Mostrar error
showControls Bool Mostrar controles UI
isFavorite Bool Estado de favorito
isDownloaded Bool Capítulo descargado
backgroundColor Color Color de fondo
readingMode ReadingMode Modo de lectura

Métodos Públicos

loadPages()
func loadPages() async

Carga las páginas del capítulo (desde local o web).

cachePage(_:image:)
func cachePage(_ page: MangaPage, image: Image) async

Cachea una página localmente (TODO: implementar).

toggleFavorite()
func toggleFavorite()

Alterna favorito del manga actual.

cycleBackgroundColor()
func cycleBackgroundColor()

Cicla entre colores de fondo (blanco/negro/sepia).

Propiedades Computadas

  • currentPageIndex: Int - Índice de página actual
  • totalPages: Int - Total de páginas

Ejemplo:

@StateObject private var viewModel = ReaderViewModel(manga: manga, chapter: chapter)

// Cargar páginas
await viewModel.loadPages()

// Ir a página específica
viewModel.currentPage = 10

// Cambiar fondo
viewModel.cycleBackgroundColor()

// Cambiar modo lectura
viewModel.readingMode = .horizontal

Views

Las vistas son componentes SwiftUI que presentan la UI.

ContentView

Vista principal que muestra la lista de mangas.

Componentes:

  • MangaRowView: Fila individual de manga
  • MangaListViewModel: ViewModel asociado

Funcionalidades:

  • Búsqueda de mangas
  • Filtros (todos/favoritos/descargados)
  • Agregar manga manualmente
  • Pull-to-refresh

MangaDetailView

Vista de detalle de un manga específico.

Componentes:

  • ChapterRowView: Fila de capítulo
  • FlowLayout: Layout de géneros
  • MangaDetailViewModel: ViewModel asociado

Funcionalidades:

  • Mostrar información del manga
  • Listar capítulos
  • Marcar favorito
  • Descargar capítulos

ReaderView

Vista de lectura de capítulos.

Componentes:

  • PageView: Vista de página individual
  • ReaderViewModel: ViewModel asociado

Funcionalidades:

  • Mostrar páginas con zoom/pan
  • Navegación entre páginas
  • Configurar fondo
  • Cambiar modo de lectura
  • Slider de navegación

Errores y Manejo de Excepciones

ScrapingError

Errores específicos del scraper.

enum ScrapingError: LocalizedError {
    case webViewNotInitialized
    case pageLoadFailed
    case noContentFound
    case parsingError

    var errorDescription: String? { ... }
}

Casos

Error Descripción Mensaje (Español)
webViewNotInitialized WKWebView no configurado "WebView no está inicializado"
pageLoadFailed Error cargando página "Error al cargar la página"
noContentFound No se encontró contenido "No se encontró contenido"
parsingError Error procesando datos "Error al procesar el contenido"

Estrategias de Manejo de Errores

  1. ViewModels: Capturan errores del scraper
  2. Views: Muestran alertas al usuario
  3. Servicios: Propagan errores con throws

Ejemplo:

do {
    let manga = try await scraper.scrapeMangaInfo(mangaSlug: slug)
    // Manejar éxito
} catch ScrapingError.webViewNotInitialized {
    // Mostrar alerta específica
    errorMessage = "Error de configuración"
} catch {
    // Error genérico
    errorMessage = error.localizedDescription
}

Última actualización: Febrero 2026 Versión: 1.0.0