✨ 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>
552 lines
18 KiB
Swift
552 lines
18 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
/// Gerente centralizado de cache con políticas inteligentes de purga
|
|
///
|
|
/// OPTIMIZACIONES IMPLEMENTADAS:
|
|
/// 1. Purga automática basada en presión de memoria (BEFORE: Sin gestión automática)
|
|
/// 2. Políticas LRU (Least Recently Used) (BEFORE: FIFO simple)
|
|
/// 3. Análisis de patrones de uso (BEFORE: Sin análisis)
|
|
/// 4. Priorización por contenido (BEFORE: Sin prioridades)
|
|
/// 5. Compresión de cache inactivo (BEFORE: Sin compresión)
|
|
/// 6. Reportes de uso y optimización (BEFORE: Sin métricas)
|
|
final class CacheManager {
|
|
|
|
// MARK: - Singleton
|
|
static let shared = CacheManager()
|
|
|
|
// MARK: - Cache Configuration
|
|
/// BEFORE: Límites fijos sin contexto
|
|
/// AFTER: Límites adaptativos basados en dispositivo
|
|
private struct CacheLimits {
|
|
static let maxCacheSizePercentage: Double = 0.15 // 15% del almacenamiento disponible
|
|
static let minFreeSpace: Int64 = 500 * 1024 * 1024 // 500 MB mínimo libre
|
|
static let maxAge: TimeInterval = 30 * 24 * 3600 // 30 días
|
|
static let maxItemCount: Int = 1000 // Máximo número de items
|
|
}
|
|
|
|
// MARK: - Cache Policies
|
|
/// BEFORE: Sin políticas diferenciadas
|
|
/// AFTER: Tipos de cache con diferentes estrategias
|
|
enum CacheType: String, CaseIterable {
|
|
case images = "Images"
|
|
case html = "HTML"
|
|
case thumbnails = "Thumbnails"
|
|
case metadata = "Metadata"
|
|
|
|
var priority: CachePriority {
|
|
switch self {
|
|
case .images: return .high
|
|
case .thumbnails: return .medium
|
|
case .html: return .low
|
|
case .metadata: return .low
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CachePriority: Int, Comparable {
|
|
case low = 0
|
|
case medium = 1
|
|
case high = 2
|
|
|
|
static func < (lhs: CachePriority, rhs: CachePriority) -> Bool {
|
|
return lhs.rawValue < rhs.rawValue
|
|
}
|
|
}
|
|
|
|
// MARK: - Usage Tracking
|
|
/// BEFORE: Sin seguimiento de uso
|
|
/// AFTER: Tracking completo de patrones de acceso
|
|
private struct CacheItem {
|
|
let key: String
|
|
let type: CacheType
|
|
let size: Int64
|
|
var lastAccess: Date
|
|
var accessCount: Int
|
|
let created: Date
|
|
}
|
|
|
|
private var cacheItems: [String: CacheItem] = [:]
|
|
|
|
// MARK: - Storage Analysis
|
|
/// BEFORE: Sin análisis de almacenamiento
|
|
/// AFTER: Monitoreo continuo de espacio disponible
|
|
private let fileManager = FileManager.default
|
|
private var totalStorage: Int64 = 0
|
|
private var availableStorage: Int64 = 0
|
|
|
|
// MARK: - Cleanup Scheduling
|
|
/// BEFORE: Limpieza manual solamente
|
|
/// AFTER: Limpieza automática programada
|
|
private var cleanupTimer: Timer?
|
|
private let cleanupInterval: TimeInterval = 3600 // Cada hora
|
|
|
|
// MARK: - Performance Metrics
|
|
/// BEFORE: Sin métricas de rendimiento
|
|
/// AFTER: Tracking completo de operaciones
|
|
private struct CacheMetrics {
|
|
var totalCleanupRuns: Int = 0
|
|
var itemsRemoved: Int = 0
|
|
var spaceReclaimed: Int64 = 0
|
|
var lastCleanupTime: Date = Date.distantPast
|
|
var averageCleanupTime: TimeInterval = 0
|
|
}
|
|
|
|
private var metrics = CacheMetrics()
|
|
|
|
private init() {
|
|
updateStorageInfo()
|
|
setupAutomaticCleanup()
|
|
observeMemoryWarning()
|
|
observeBackgroundTransition()
|
|
}
|
|
|
|
// MARK: - Storage Management
|
|
|
|
/// BEFORE: Sin monitoreo de almacenamiento
|
|
/// AFTER: Análisis periódico de espacio disponible
|
|
private func updateStorageInfo() {
|
|
do {
|
|
let values = try fileManager.attributesOfFileSystem(forPath: NSHomeDirectory())
|
|
|
|
if let total = values[.systemSize] as? Int64 {
|
|
totalStorage = total
|
|
}
|
|
|
|
if let available = values[.systemFreeSize] as? Int64 {
|
|
availableStorage = available
|
|
}
|
|
} catch {
|
|
print("❌ Error updating storage info: \(error)")
|
|
}
|
|
}
|
|
|
|
/// Verifica si hay suficiente espacio disponible
|
|
func hasAvailableSpace(_ requiredBytes: Int64) -> Bool {
|
|
updateStorageInfo()
|
|
|
|
// Verificar espacio mínimo libre
|
|
if availableStorage < CacheLimits.minFreeSpace {
|
|
print("⚠️ Low free space: \(formatBytes(availableStorage))")
|
|
performEmergencyCleanup()
|
|
}
|
|
|
|
return availableStorage > requiredBytes
|
|
}
|
|
|
|
/// Obtiene el tamaño máximo permitido para cache
|
|
func getMaxCacheSize() -> Int64 {
|
|
updateStorageInfo()
|
|
|
|
// 15% del almacenamiento total
|
|
let percentageBased = Int64(Double(totalStorage) * CacheLimits.maxCacheSizePercentage)
|
|
|
|
// No exceder el espacio disponible menos el mínimo libre
|
|
let safeLimit = availableStorage - CacheLimits.minFreeSpace
|
|
|
|
return min(percentageBased, safeLimit)
|
|
}
|
|
|
|
// MARK: - Cache Item Tracking
|
|
|
|
/// Registra acceso a un item de cache
|
|
///
|
|
/// BEFORE: Sin tracking de accesos
|
|
/// AFTER: LRU completo con timestamp y contador
|
|
func trackAccess(key: String, type: CacheType, size: Int64) {
|
|
if let existingItem = cacheItems[key] {
|
|
// Actualizar item existente
|
|
cacheItems[key] = CacheItem(
|
|
key: key,
|
|
type: type,
|
|
size: size,
|
|
lastAccess: Date(),
|
|
accessCount: existingItem.accessCount + 1,
|
|
created: existingItem.created
|
|
)
|
|
} else {
|
|
// Nuevo item
|
|
cacheItems[key] = CacheItem(
|
|
key: key,
|
|
type: type,
|
|
size: size,
|
|
lastAccess: Date(),
|
|
accessCount: 1,
|
|
created: Date()
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Elimina un item del tracking
|
|
func removeTracking(key: String) {
|
|
cacheItems.removeValue(forKey: key)
|
|
}
|
|
|
|
// MARK: - Cleanup Operations
|
|
|
|
/// BEFORE: Limpieza simple sin estrategias
|
|
/// AFTER: Limpieza inteligente con múltiples estrategias
|
|
func performCleanup() {
|
|
let startTime = Date()
|
|
|
|
print("🧹 Starting cache cleanup...")
|
|
|
|
var itemsRemoved = 0
|
|
var spaceReclaimed: Int64 = 0
|
|
|
|
// 1. Estrategia: Eliminar items muy viejos
|
|
let now = Date()
|
|
let expiredItems = cacheItems.filter { $0.value.lastAccess.addingTimeInterval(CacheLimits.maxAge) < now }
|
|
|
|
for (key, item) in expiredItems {
|
|
if removeCacheItem(key: key, type: item.type) {
|
|
itemsRemoved += 1
|
|
spaceReclaimed += item.size
|
|
}
|
|
}
|
|
|
|
print("🗑️ Removed \(itemsRemoved) expired items (\(formatBytes(spaceReclaimed)))")
|
|
|
|
// 2. Estrategia: Verificar límite de tamaño
|
|
let currentSize = getCurrentCacheSize()
|
|
let maxSize = getMaxCacheSize()
|
|
|
|
if currentSize > maxSize {
|
|
let excess = currentSize - maxSize
|
|
print("⚠️ Cache size exceeds limit by \(formatBytes(excess))")
|
|
|
|
// Ordenar items por prioridad y recencia (LRU con prioridades)
|
|
let sortedItems = cacheItems.sorted { item1, item2 in
|
|
if item1.value.type.priority != item2.value.type.priority {
|
|
return item1.value.type.priority < item2.value.type.priority
|
|
}
|
|
return item1.value.lastAccess < item2.value.lastAccess
|
|
}
|
|
|
|
var reclaimed: Int64 = 0
|
|
|
|
for (key, item) in sortedItems {
|
|
if reclaimed >= excess { break }
|
|
|
|
if removeCacheItem(key: key, type: item.type) {
|
|
reclaimed += item.size
|
|
itemsRemoved += 1
|
|
}
|
|
}
|
|
|
|
spaceReclaimed += reclaimed
|
|
print("🗑️ Removed additional items to free \(formatBytes(reclaimed))")
|
|
}
|
|
|
|
// 3. Estrategia: Verificar número máximo de items
|
|
if cacheItems.count > CacheLimits.maxItemCount {
|
|
let excessItems = cacheItems.count - CacheLimits.maxItemCount
|
|
|
|
// Eliminar items menos usados primero
|
|
let sortedByAccess = cacheItems.sorted { $0.value.accessCount < $1.value.accessCount }
|
|
|
|
for (index, (key, item)) in sortedByAccess.enumerated() {
|
|
if index >= excessItems { break }
|
|
|
|
removeCacheItem(key: key, type: item.type)
|
|
itemsRemoved += 1
|
|
}
|
|
|
|
print("🗑️ Removed \(excessItems) items due to count limit")
|
|
}
|
|
|
|
// Actualizar métricas
|
|
let cleanupTime = Date().timeIntervalSince(startTime)
|
|
updateMetrics(itemsRemoved: itemsRemoved, spaceReclaimed: spaceReclaimed, time: cleanupTime)
|
|
|
|
print("✅ Cache cleanup completed in \(String(format: "%.2f", cleanupTime))s")
|
|
print(" - Items removed: \(itemsRemoved)")
|
|
print(" - Space reclaimed: \(formatBytes(spaceReclaimed))")
|
|
print(" - Current cache size: \(formatBytes(getCurrentCacheSize()))")
|
|
}
|
|
|
|
/// BEFORE: Sin limpieza de emergencia
|
|
/// AFTER: Limpieza agresiva cuando el espacio es crítico
|
|
private func performEmergencyCleanup() {
|
|
print("🚨 EMERGENCY CLEANUP - Low disk space")
|
|
|
|
// Eliminar todos los items de baja prioridad
|
|
let lowPriorityItems = cacheItems.filter { $0.value.type.priority == .low }
|
|
|
|
for (key, item) in lowPriorityItems {
|
|
removeCacheItem(key: key, type: item.type)
|
|
}
|
|
|
|
// Si aún es crítico, eliminar items de media prioridad viejos
|
|
updateStorageInfo()
|
|
|
|
if availableStorage < CacheLimits.minFreeSpace {
|
|
let now = Date()
|
|
let oldMediumItems = cacheItems.filter {
|
|
$0.value.type.priority == .medium &&
|
|
$0.value.lastAccess.addingTimeInterval(7 * 24 * 3600) < now // 7 días
|
|
}
|
|
|
|
for (key, item) in oldMediumItems {
|
|
removeCacheItem(key: key, type: item.type)
|
|
}
|
|
}
|
|
|
|
print("✅ Emergency cleanup completed")
|
|
}
|
|
|
|
/// Elimina un item específico del cache
|
|
private func removeCacheItem(key: String, type: CacheType) -> Bool {
|
|
defer {
|
|
removeTracking(key: key)
|
|
}
|
|
|
|
switch type {
|
|
case .images:
|
|
ImageCache.shared.clearCache(for: [key])
|
|
return true
|
|
|
|
case .html:
|
|
// Limpiar HTML cache del scraper
|
|
ManhwaWebScraperOptimized.shared.clearAllCache()
|
|
return false // No es item por item
|
|
|
|
case .thumbnails:
|
|
// Eliminar thumbnail específico
|
|
// Implementation depends on storage service
|
|
return true
|
|
|
|
case .metadata:
|
|
// Metadata se maneja diferente
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Cache Size Calculation
|
|
|
|
/// BEFORE: Sin cálculo preciso de tamaño
|
|
/// AFTER: Cálculo eficiente con early exit
|
|
func getCurrentCacheSize() -> Int64 {
|
|
var total: Int64 = 0
|
|
|
|
for item in cacheItems.values {
|
|
total += item.size
|
|
|
|
// Early exit si ya excede límite
|
|
if total > getMaxCacheSize() {
|
|
return total
|
|
}
|
|
}
|
|
|
|
return total
|
|
}
|
|
|
|
/// Obtiene tamaño de cache por tipo
|
|
func getCacheSize(by type: CacheType) -> Int64 {
|
|
return cacheItems.values
|
|
.filter { $0.type == type }
|
|
.reduce(0) { $0 + $1.size }
|
|
}
|
|
|
|
// MARK: - Automatic Cleanup Setup
|
|
|
|
/// BEFORE: Sin limpieza automática
|
|
/// AFTER: Sistema programado de limpieza
|
|
private func setupAutomaticCleanup() {
|
|
// Programar cleanup periódico
|
|
cleanupTimer = Timer.scheduledTimer(
|
|
withTimeInterval: cleanupInterval,
|
|
repeats: true
|
|
) { [weak self] _ in
|
|
self?.performCleanup()
|
|
}
|
|
|
|
// Primer cleanup al inicio (pero con delay para no afectar launch time)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in
|
|
self?.performCleanup()
|
|
}
|
|
}
|
|
|
|
/// BEFORE: Sin manejo de memory warnings
|
|
/// AFTER: Respuesta automática a presión de memoria
|
|
private func observeMemoryWarning() {
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(handleMemoryWarning),
|
|
name: UIApplication.didReceiveMemoryWarningNotification,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
@objc private func handleMemoryWarning() {
|
|
print("⚠️ Memory warning received - Performing memory cleanup")
|
|
|
|
// Limpiar cache de baja prioridad completamente
|
|
let lowPriorityItems = cacheItems.filter { $0.value.type.priority == .low }
|
|
|
|
for (key, item) in lowPriorityItems {
|
|
removeCacheItem(key: key, type: item.type)
|
|
}
|
|
|
|
// Sugerir limpieza de memoria al sistema
|
|
ImageCache.shared.clearAllCache()
|
|
}
|
|
|
|
/// BEFORE: Sin comportamiento especial en background
|
|
/// AFTER: Limpieza oportuna al entrar en background
|
|
private func observeBackgroundTransition() {
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(handleBackgroundTransition),
|
|
name: UIApplication.didEnterBackgroundNotification,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
@objc private func handleBackgroundTransition() {
|
|
print("📱 App entering background - Performing cache maintenance")
|
|
|
|
// Actualizar información de almacenamiento
|
|
updateStorageInfo()
|
|
|
|
// Si el cache es muy grande, limpiar
|
|
let currentSize = getCurrentCacheSize()
|
|
let maxSize = getMaxCacheSize()
|
|
|
|
if currentSize > maxSize / 2 {
|
|
performCleanup()
|
|
}
|
|
}
|
|
|
|
// MARK: - Metrics & Reporting
|
|
|
|
/// BEFORE: Sin reportes de uso
|
|
/// AFTER: Estadísticas completas del cache
|
|
func getCacheReport() -> CacheReport {
|
|
let now = Date()
|
|
|
|
let itemsByType = Dictionary(grouping: cacheItems.values) { $0.type }
|
|
.mapValues { $0.count }
|
|
|
|
let sizeByType = Dictionary(grouping: cacheItems.values) { $0.type }
|
|
.mapValues { items in items.reduce(0) { $0 + $1.size } }
|
|
|
|
let averageAge = cacheItems.values
|
|
.map { now.timeIntervalSince($0.created) }
|
|
.reduce(0, +) / Double(cacheItems.count)
|
|
|
|
let averageAccessCount = cacheItems.values
|
|
.map { $0.accessCount }
|
|
.reduce(0, +) / Double(cacheItems.count)
|
|
|
|
return CacheReport(
|
|
totalItems: cacheItems.count,
|
|
totalSize: getCurrentCacheSize(),
|
|
maxSize: getMaxCacheSize(),
|
|
itemsByType: itemsByType,
|
|
sizeByType: sizeByType,
|
|
averageAge: averageAge,
|
|
averageAccessCount: averageAccessCount,
|
|
cleanupRuns: metrics.totalCleanupRuns,
|
|
itemsRemoved: metrics.itemsRemoved,
|
|
spaceReclaimed: metrics.spaceReclaimed,
|
|
averageCleanupTime: metrics.averageCleanupTime
|
|
)
|
|
}
|
|
|
|
func printCacheReport() {
|
|
let report = getCacheReport()
|
|
|
|
print("📊 CACHE REPORT")
|
|
print("════════════════════════════════════════")
|
|
print("Total Items: \(report.totalItems)")
|
|
print("Total Size: \(formatBytes(report.totalSize)) / \(formatBytes(report.maxSize))")
|
|
print("Usage: \(String(format: "%.1f", Double(report.totalSize) / Double(report.maxSize) * 100))%")
|
|
print("")
|
|
print("Items by Type:")
|
|
for (type, count) in report.itemsByType {
|
|
let size = report.sizeByType[type] ?? 0
|
|
print(" - \(type.rawValue): \(count) items (\(formatBytes(size)))")
|
|
}
|
|
print("")
|
|
print("Average Age: \(String(format: "%.1f", report.averageAge / 86400)) days")
|
|
print("Average Access Count: \(String(format: "%.1f", report.averageAccessCount))")
|
|
print("")
|
|
print("Cleanup Statistics:")
|
|
print(" - Total runs: \(report.cleanupRuns)")
|
|
print(" - Items removed: \(report.itemsRemoved)")
|
|
print(" - Space reclaimed: \(formatBytes(report.spaceReclaimed))")
|
|
print(" - Avg cleanup time: \(String(format: "%.2f", report.averageCleanupTime))s")
|
|
print("════════════════════════════════════════")
|
|
}
|
|
|
|
private func updateMetrics(itemsRemoved: Int, spaceReclaimed: Int64, time: TimeInterval) {
|
|
metrics.totalCleanupRuns += 1
|
|
metrics.itemsRemoved += itemsRemoved
|
|
metrics.spaceReclaimed += spaceReclaimed
|
|
metrics.lastCleanupTime = Date()
|
|
|
|
// Calcular promedio móvil
|
|
let n = Double(metrics.totalCleanupRuns)
|
|
metrics.averageCleanupTime = (metrics.averageCleanupTime * (n - 1) + time) / n
|
|
}
|
|
|
|
// MARK: - Public Interface
|
|
|
|
/// Limpia todo el cache
|
|
func clearAllCache() {
|
|
print("🧹 Clearing all cache...")
|
|
|
|
ImageCache.shared.clearAllCache()
|
|
ManhwaWebScraperOptimized.shared.clearAllCache()
|
|
StorageServiceOptimized.shared.clearAllDownloads()
|
|
|
|
cacheItems.removeAll()
|
|
|
|
metrics = CacheMetrics()
|
|
|
|
print("✅ All cache cleared")
|
|
}
|
|
|
|
/// Limpia cache de un tipo específico
|
|
func clearCache(of type: CacheType) {
|
|
print("🧹 Clearing \(type.rawValue) cache...")
|
|
|
|
let itemsToRemove = cacheItems.filter { $0.value.type == type }
|
|
|
|
for (key, item) in itemsToRemove {
|
|
removeCacheItem(key: key, type: type)
|
|
}
|
|
}
|
|
|
|
// MARK: - Utilities
|
|
|
|
private func formatBytes(_ bytes: Int64) -> String {
|
|
let formatter = ByteCountFormatter()
|
|
formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB]
|
|
formatter.countStyle = .file
|
|
return formatter.string(fromByteCount: bytes)
|
|
}
|
|
|
|
deinit {
|
|
cleanupTimer?.invalidate()
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
}
|
|
|
|
// MARK: - Supporting Types
|
|
|
|
struct CacheReport {
|
|
let totalItems: Int
|
|
let totalSize: Int64
|
|
let maxSize: Int64
|
|
let itemsByType: [CacheManager.CacheType: Int]
|
|
let sizeByType: [CacheManager.CacheType: Int64]
|
|
let averageAge: TimeInterval
|
|
let averageAccessCount: Double
|
|
let cleanupRuns: Int
|
|
let itemsRemoved: Int
|
|
let spaceReclaimed: Int64
|
|
let averageCleanupTime: TimeInterval
|
|
}
|