✨ 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>
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
Doublepara 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
truesi 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.webViewNotInitializedScrapingError.pageLoadFailedScrapingError.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:
- Construye URL:
https://manhwaweb.com/manga/{slug} - Carga URL en WKWebView
- Espera 3 segundos a que JavaScript renderice
- 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)
- Título (de
- 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.webViewNotInitializedScrapingError.pageLoadFailedScrapingError.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:
- Carga página del manga
- Ejecuta JavaScript que:
- Busca todos los links
/leer/* - Extrae número de capítulo via regex
- Filtra duplicados
- Ordena descendente
- Busca todos los links
- 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.webViewNotInitializedScrapingError.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:
- Carga URL del capítulo
- Espera 5 segundos (para imágenes)
- Ejecuta JavaScript que:
- Selecciona todas las etiquetas
<img> - Filtra elementos de UI (avatars, icons, logos)
- Elimina duplicados
- Selecciona todas las etiquetas
- 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 cargarwaitForImages:Bool- Sitrue, espera 5 segundos; sifalse, 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 guardarmangaSlug:String- Slug del mangachapterNumber:Int- Número del capítulopageIndex: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 actualtotalPages: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 mangaMangaListViewModel: 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ítuloFlowLayout: Layout de génerosMangaDetailViewModel: 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 individualReaderViewModel: 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
- ViewModels: Capturan errores del scraper
- Views: Muestran alertas al usuario
- 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