From 1e909ca9ebb1a058bda266b34c3de3ed22959197 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:23:19 +0200 Subject: [PATCH] test cache stuff removed --- Sora/Managers/EpisodeMetadataManager.swift | 646 ------------------ Sora/Managers/ImagePrefetchManager.swift | 134 ---- Sora/Managers/ImageUpscaler.swift | 165 ----- Sora/Managers/PerformanceMonitor.swift | 510 -------------- Sora/SoraApp.swift | 3 - Sora/Utils/Cache/EpisodeMetadata.swift | 45 -- Sora/Utils/Cache/KingfisherManager.swift | 95 --- Sora/Utils/Cache/MetadataCacheManager.swift | 276 -------- .../EpisodeCell/EpisodeCell.swift | 36 - Sora/Views/MediaInfoView/MediaInfoView.swift | 36 - .../SettingsSubViews/SettingsViewData.swift | 169 +---- Sulfur.xcodeproj/project.pbxproj | 44 -- 12 files changed, 35 insertions(+), 2124 deletions(-) delete mode 100644 Sora/Managers/EpisodeMetadataManager.swift delete mode 100644 Sora/Managers/ImagePrefetchManager.swift delete mode 100644 Sora/Managers/ImageUpscaler.swift delete mode 100644 Sora/Managers/PerformanceMonitor.swift delete mode 100644 Sora/Utils/Cache/EpisodeMetadata.swift delete mode 100644 Sora/Utils/Cache/KingfisherManager.swift delete mode 100644 Sora/Utils/Cache/MetadataCacheManager.swift diff --git a/Sora/Managers/EpisodeMetadataManager.swift b/Sora/Managers/EpisodeMetadataManager.swift deleted file mode 100644 index 05dc58d..0000000 --- a/Sora/Managers/EpisodeMetadataManager.swift +++ /dev/null @@ -1,646 +0,0 @@ -// -// EpisodeMetadataManager.swift -// Sora -// -// Created by doomsboygaming on 5/22/25 -// - -import Foundation -import Combine - -/// A model representing episode metadata -struct EpisodeMetadataInfo: Codable, Equatable { - let title: [String: String] - let imageUrl: String - let anilistId: Int - let episodeNumber: Int - - var cacheKey: String { - return "anilist_\(anilistId)_episode_\(episodeNumber)" - } -} - -/// Status of a metadata fetch request -enum MetadataFetchStatus { - case notRequested - case fetching - case fetched(EpisodeMetadataInfo) - case failed(Error) -} - -/// Central manager for fetching, caching, and prefetching episode metadata -class EpisodeMetadataManager: ObservableObject { - static let shared = EpisodeMetadataManager() - - private init() { - // Initialize any resources here - Logger.shared.log("EpisodeMetadataManager initialized", type: "Info") - } - - // Published properties that trigger UI updates - @Published private var metadataCache: [String: MetadataFetchStatus] = [:] - - // In-flight requests to prevent duplicate API calls - private var activeRequests: [String: AnyCancellable] = [:] - - // Queue for managing concurrent requests - private let fetchQueue = DispatchQueue(label: "com.sora.metadataFetch", qos: .userInitiated, attributes: .concurrent) - - // Add retry configuration properties - private let maxRetryAttempts = 3 - private let initialBackoffDelay: TimeInterval = 1.0 // in seconds - private var currentRetryAttempts: [String: Int] = [:] // Track retry attempts by cache key - - // MARK: - Public Interface - - /// Fetch metadata for a single episode - /// - Parameters: - /// - anilistId: The Anilist ID of the anime - /// - episodeNumber: The episode number to fetch - /// - completion: Callback with the result - func fetchMetadata(anilistId: Int, episodeNumber: Int, completion: @escaping (Result) -> Void) { - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - - // Check if we already have this metadata - if let existingStatus = metadataCache[cacheKey] { - switch existingStatus { - case .fetched(let metadata): - // Return cached data immediately - completion(.success(metadata)) - return - - case .fetching: - // Already fetching, will be notified via publisher - // Set up a listener for when this request completes - waitForRequest(cacheKey: cacheKey, completion: completion) - return - - case .failed: - // Previous attempt failed, try again - break - - case .notRequested: - // Should not happen but continue to fetch - break - } - } - - // Check persistent cache - if let cachedData = MetadataCacheManager.shared.getMetadata(forKey: cacheKey), - let metadata = EpisodeMetadata.fromData(cachedData) { - - let metadataInfo = EpisodeMetadataInfo( - title: metadata.title, - imageUrl: metadata.imageUrl, - anilistId: anilistId, - episodeNumber: episodeNumber - ) - - // Update memory cache - DispatchQueue.main.async { - self.metadataCache[cacheKey] = .fetched(metadataInfo) - } - - completion(.success(metadataInfo)) - return - } - - // Need to fetch from network - DispatchQueue.main.async { - self.metadataCache[cacheKey] = .fetching - } - - performFetch(anilistId: anilistId, episodeNumber: episodeNumber, cacheKey: cacheKey, completion: completion) - } - - /// Fetch metadata for multiple episodes in batch - /// - Parameters: - /// - anilistId: The Anilist ID of the anime - /// - episodeNumbers: Array of episode numbers to fetch - func batchFetchMetadata(anilistId: Int, episodeNumbers: [Int]) { - // First check which episodes we need to fetch - let episodesToFetch = episodeNumbers.filter { episodeNumber in - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - if let status = metadataCache[cacheKey] { - switch status { - case .fetched, .fetching: - return false - default: - return true - } - } - return true - } - - guard !episodesToFetch.isEmpty else { - Logger.shared.log("No new episodes to fetch in batch", type: "Debug") - return - } - - // Mark all as fetching - for episodeNumber in episodesToFetch { - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - DispatchQueue.main.async { - self.metadataCache[cacheKey] = .fetching - } - } - - // Perform batch fetch - fetchBatchFromNetwork(anilistId: anilistId, episodeNumbers: episodesToFetch) - } - - /// Prefetch metadata for a range of episodes - /// - Parameters: - /// - anilistId: The Anilist ID of the anime - /// - startEpisode: The starting episode number - /// - count: How many episodes to prefetch - func prefetchMetadata(anilistId: Int, startEpisode: Int, count: Int = 5) { - let episodeNumbers = Array(startEpisode..<(startEpisode + count)) - batchFetchMetadata(anilistId: anilistId, episodeNumbers: episodeNumbers) - } - - /// Get metadata for an episode (non-blocking, returns immediately from cache) - /// - Parameters: - /// - anilistId: The Anilist ID of the anime - /// - episodeNumber: The episode number - /// - Returns: The metadata fetch status - func getMetadataStatus(anilistId: Int, episodeNumber: Int) -> MetadataFetchStatus { - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - return metadataCache[cacheKey] ?? .notRequested - } - - // MARK: - Private Methods - - private func performFetch(anilistId: Int, episodeNumber: Int, cacheKey: String, completion: @escaping (Result) -> Void) { - // Check if there's already an active request for this metadata - if activeRequests[cacheKey] != nil { - // Already fetching, wait for it to complete - waitForRequest(cacheKey: cacheKey, completion: completion) - return - } - - // Reset retry attempts if this is a new fetch - if currentRetryAttempts[cacheKey] == nil { - currentRetryAttempts[cacheKey] = 0 - } - - // Create API request - guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(anilistId)") else { - let error = NSError(domain: "com.sora.metadata", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) - DispatchQueue.main.async { - self.metadataCache[cacheKey] = .failed(error) - } - completion(.failure(error)) - return - } - - Logger.shared.log("Fetching metadata for episode \(episodeNumber) from network", type: "Debug") - - // Create publisher for the request - let publisher = URLSession.custom.dataTaskPublisher(for: url) - .subscribe(on: fetchQueue) - .tryMap { [weak self] data, response -> EpisodeMetadataInfo in - guard let self = self else { - throw NSError(domain: "com.sora.metadata", code: 4, - userInfo: [NSLocalizedDescriptionKey: "Manager instance released"]) - } - - // Validate response - guard let httpResponse = response as? HTTPURLResponse, - httpResponse.statusCode == 200 else { - throw NSError(domain: "com.sora.metadata", code: 2, - userInfo: [NSLocalizedDescriptionKey: "Invalid response"]) - } - - // Parse JSON - let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - guard let json = jsonObject as? [String: Any] else { - throw NSError(domain: "com.sora.metadata", code: 3, - userInfo: [NSLocalizedDescriptionKey: "Invalid data format"]) - } - - // Check for episodes object - guard let episodes = json["episodes"] as? [String: Any] else { - Logger.shared.log("Missing 'episodes' object in response for anilistId: \(anilistId)", type: "Error") - throw NSError(domain: "com.sora.metadata", code: 3, - userInfo: [NSLocalizedDescriptionKey: "Missing episodes object in response"]) - } - - // Check if episode exists in response - let episodeKey = "\(episodeNumber)" - guard let episodeDetails = episodes[episodeKey] as? [String: Any] else { - Logger.shared.log("Episode \(episodeNumber) not found in response for anilistId: \(anilistId)", type: "Error") - throw NSError(domain: "com.sora.metadata", code: 5, - userInfo: [NSLocalizedDescriptionKey: "Episode \(episodeNumber) not found in response"]) - } - - // Extract available fields, log if they're missing - var title: [String: String] = [:] - var image: String = "" - var missingFields: [String] = [] - - // Try to get title - if let titleData = episodeDetails["title"] as? [String: String] { - title = titleData - - // Check if we have valid title values - if title.isEmpty || title.values.allSatisfy({ $0.isEmpty }) { - missingFields.append("title (all values empty)") - } - } else { - missingFields.append("title") - // Create default empty title dictionary - title = ["en": "Episode \(episodeNumber)"] - } - - // Try to get image - if let imageUrl = episodeDetails["image"] as? String { - image = imageUrl - - if imageUrl.isEmpty { - missingFields.append("image (empty string)") - } - } else { - missingFields.append("image") - // Use a default placeholder image - image = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png" - } - - // Log missing fields but continue processing - if !missingFields.isEmpty { - Logger.shared.log("Episode \(episodeNumber) for anilistId \(anilistId) missing fields: \(missingFields.joined(separator: ", "))", type: "Warning") - } - - // Create metadata object with whatever we have - let metadataInfo = EpisodeMetadataInfo( - title: title, - imageUrl: image, - anilistId: anilistId, - episodeNumber: episodeNumber - ) - - // Cache the metadata - if MetadataCacheManager.shared.isCachingEnabled { - let metadata = EpisodeMetadata( - title: title, - imageUrl: image, - anilistId: anilistId, - episodeNumber: episodeNumber - ) - - if let metadataData = metadata.toData() { - MetadataCacheManager.shared.storeMetadata( - metadataData, - forKey: cacheKey - ) - Logger.shared.log("Cached metadata for episode \(episodeNumber)", type: "Debug") - } - } - - // Reset retry count on success (even with missing fields) - self.currentRetryAttempts.removeValue(forKey: cacheKey) - - return metadataInfo - } - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] result in - // Handle completion - guard let self = self else { return } - - switch result { - case .finished: - break - case .failure(let error): - // Handle retry logic - var shouldRetry = false - let currentAttempt = self.currentRetryAttempts[cacheKey] ?? 0 - - // Check if we should retry based on the error and attempt count - if currentAttempt < self.maxRetryAttempts { - // Increment attempt counter - let nextAttempt = currentAttempt + 1 - self.currentRetryAttempts[cacheKey] = nextAttempt - - // Calculate backoff delay using exponential backoff - let backoffDelay = self.initialBackoffDelay * pow(2.0, Double(currentAttempt)) - - Logger.shared.log("Metadata fetch failed, retrying (attempt \(nextAttempt)/\(self.maxRetryAttempts)) in \(backoffDelay) seconds", type: "Debug") - - // Schedule retry after backoff delay - DispatchQueue.main.asyncAfter(deadline: .now() + backoffDelay) { - // Remove the current request before retrying - self.activeRequests.removeValue(forKey: cacheKey) - self.performFetch(anilistId: anilistId, episodeNumber: episodeNumber, cacheKey: cacheKey, completion: completion) - } - shouldRetry = true - } else { - // Max retries reached - Logger.shared.log("Metadata fetch failed after \(self.maxRetryAttempts) attempts: \(error.localizedDescription)", type: "Error") - self.currentRetryAttempts.removeValue(forKey: cacheKey) - } - - if !shouldRetry { - // Update cache with error - self.metadataCache[cacheKey] = .failed(error) - completion(.failure(error)) - // Remove from active requests - self.activeRequests.removeValue(forKey: cacheKey) - } - } - }, receiveValue: { [weak self] metadataInfo in - // Update cache with result - self?.metadataCache[cacheKey] = .fetched(metadataInfo) - completion(.success(metadataInfo)) - - // Remove from active requests - self?.activeRequests.removeValue(forKey: cacheKey) - }) - - // Store publisher in active requests - activeRequests[cacheKey] = publisher - } - - private func fetchBatchFromNetwork(anilistId: Int, episodeNumbers: [Int]) { - // This API returns all episodes for a show in one call, so we only need one request - guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(anilistId)") else { - Logger.shared.log("Invalid URL for batch fetch", type: "Error") - return - } - - Logger.shared.log("Batch fetching \(episodeNumbers.count) episodes from network", type: "Debug") - - let batchCacheKey = "batch_\(anilistId)_\(episodeNumbers.map { String($0) }.joined(separator: "_"))" - - // Reset retry attempts if this is a new fetch - if currentRetryAttempts[batchCacheKey] == nil { - currentRetryAttempts[batchCacheKey] = 0 - } - - // Create publisher for the request - let publisher = URLSession.custom.dataTaskPublisher(for: url) - .subscribe(on: fetchQueue) - .tryMap { [weak self] data, response -> [Int: EpisodeMetadataInfo] in - guard let self = self else { - throw NSError(domain: "com.sora.metadata", code: 4, - userInfo: [NSLocalizedDescriptionKey: "Manager instance released"]) - } - - // Validate response - guard let httpResponse = response as? HTTPURLResponse, - httpResponse.statusCode == 200 else { - throw NSError(domain: "com.sora.metadata", code: 2, - userInfo: [NSLocalizedDescriptionKey: "Invalid response"]) - } - - // Parse JSON - let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - guard let json = jsonObject as? [String: Any] else { - throw NSError(domain: "com.sora.metadata", code: 3, - userInfo: [NSLocalizedDescriptionKey: "Invalid data format"]) - } - - guard let episodes = json["episodes"] as? [String: Any] else { - Logger.shared.log("Missing 'episodes' object in response for anilistId: \(anilistId)", type: "Error") - throw NSError(domain: "com.sora.metadata", code: 3, - userInfo: [NSLocalizedDescriptionKey: "Missing episodes object in response"]) - } - - // Check if we have at least one requested episode - let hasAnyRequestedEpisode = episodeNumbers.contains { episodeNumber in - return episodes["\(episodeNumber)"] != nil - } - - if !hasAnyRequestedEpisode { - Logger.shared.log("None of the requested episodes were found for anilistId: \(anilistId)", type: "Error") - throw NSError(domain: "com.sora.metadata", code: 5, - userInfo: [NSLocalizedDescriptionKey: "None of the requested episodes were found"]) - } - - // Process each requested episode - var results: [Int: EpisodeMetadataInfo] = [:] - var missingEpisodes: [Int] = [] - var episodesWithMissingFields: [String] = [] - - for episodeNumber in episodeNumbers { - let episodeKey = "\(episodeNumber)" - - // Check if this episode exists in the response - if let episodeDetails = episodes[episodeKey] as? [String: Any] { - var title: [String: String] = [:] - var image: String = "" - var missingFields: [String] = [] - - // Try to get title - if let titleData = episodeDetails["title"] as? [String: String] { - title = titleData - - // Check if we have valid title values - if title.isEmpty || title.values.allSatisfy({ $0.isEmpty }) { - missingFields.append("title (all values empty)") - } - } else { - missingFields.append("title") - // Create default empty title dictionary - title = ["en": "Episode \(episodeNumber)"] - } - - // Try to get image - if let imageUrl = episodeDetails["image"] as? String { - image = imageUrl - - if imageUrl.isEmpty { - missingFields.append("image (empty string)") - } - } else { - missingFields.append("image") - // Use a default placeholder image - image = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png" - } - - // Log if we're missing any fields - if !missingFields.isEmpty { - episodesWithMissingFields.append("Episode \(episodeNumber): missing \(missingFields.joined(separator: ", "))") - } - - // Create metadata object with whatever we have - let metadataInfo = EpisodeMetadataInfo( - title: title, - imageUrl: image, - anilistId: anilistId, - episodeNumber: episodeNumber - ) - - results[episodeNumber] = metadataInfo - - // Cache the metadata - if MetadataCacheManager.shared.isCachingEnabled { - let metadata = EpisodeMetadata( - title: title, - imageUrl: image, - anilistId: anilistId, - episodeNumber: episodeNumber - ) - - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - if let metadataData = metadata.toData() { - MetadataCacheManager.shared.storeMetadata( - metadataData, - forKey: cacheKey - ) - } - } - } else { - missingEpisodes.append(episodeNumber) - } - } - - // Log information about missing episodes - if !missingEpisodes.isEmpty { - Logger.shared.log("Episodes not found in response: \(missingEpisodes.map { String($0) }.joined(separator: ", "))", type: "Warning") - } - - // Log information about episodes with missing fields - if !episodesWithMissingFields.isEmpty { - Logger.shared.log("Episodes with missing fields: \(episodesWithMissingFields.joined(separator: "; "))", type: "Warning") - } - - // If we didn't get data for all requested episodes but got some, consider it a partial success - if results.count < episodeNumbers.count && results.count > 0 { - Logger.shared.log("Partial data received: \(results.count)/\(episodeNumbers.count) episodes", type: "Warning") - } - - // If we didn't get any valid results, throw an error to trigger retry - if results.isEmpty { - throw NSError(domain: "com.sora.metadata", code: 7, - userInfo: [NSLocalizedDescriptionKey: "No valid episode data found in response"]) - } - - // Reset retry count on success (even partial) - self.currentRetryAttempts.removeValue(forKey: batchCacheKey) - - return results - } - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] result in - // Handle completion - guard let self = self else { return } - - switch result { - case .finished: - break - case .failure(let error): - // Handle retry logic - var shouldRetry = false - let currentAttempt = self.currentRetryAttempts[batchCacheKey] ?? 0 - - // Check if we should retry based on the error and attempt count - if currentAttempt < self.maxRetryAttempts { - // Increment attempt counter - let nextAttempt = currentAttempt + 1 - self.currentRetryAttempts[batchCacheKey] = nextAttempt - - // Calculate backoff delay using exponential backoff - let backoffDelay = self.initialBackoffDelay * pow(2.0, Double(currentAttempt)) - - Logger.shared.log("Batch fetch failed, retrying (attempt \(nextAttempt)/\(self.maxRetryAttempts)) in \(backoffDelay) seconds", type: "Debug") - - // Schedule retry after backoff delay - DispatchQueue.main.asyncAfter(deadline: .now() + backoffDelay) { - // Remove the current request before retrying - self.activeRequests.removeValue(forKey: batchCacheKey) - self.fetchBatchFromNetwork(anilistId: anilistId, episodeNumbers: episodeNumbers) - } - shouldRetry = true - } else { - // Max retries reached - Logger.shared.log("Batch fetch failed after \(self.maxRetryAttempts) attempts: \(error.localizedDescription)", type: "Error") - self.currentRetryAttempts.removeValue(forKey: batchCacheKey) - - // Update all requested episodes with error - for episodeNumber in episodeNumbers { - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - self.metadataCache[cacheKey] = .failed(error) - } - } - - if !shouldRetry { - // Remove from active requests - self.activeRequests.removeValue(forKey: batchCacheKey) - } - } - }, receiveValue: { [weak self] results in - // Update cache with results - for (episodeNumber, metadataInfo) in results { - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - self?.metadataCache[cacheKey] = .fetched(metadataInfo) - } - - // Log the results - Logger.shared.log("Batch fetch completed with \(results.count) episodes", type: "Debug") - - // Remove from active requests - self?.activeRequests.removeValue(forKey: batchCacheKey) - }) - - // Store publisher in active requests - activeRequests[batchCacheKey] = publisher - } - - private func waitForRequest(cacheKey: String, completion: @escaping (Result) -> Void) { - // Set up a timer to check the cache periodically - let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in - guard let self = self else { - timer.invalidate() - return - } - - if let status = self.metadataCache[cacheKey] { - switch status { - case .fetched(let metadata): - // Request completed successfully - timer.invalidate() - completion(.success(metadata)) - case .failed(let error): - // Request failed - timer.invalidate() - completion(.failure(error)) - case .fetching, .notRequested: - // Still in progress - break - } - } - } - - // Ensure timer fires even when scrolling - RunLoop.current.add(timer, forMode: .common) - } -} - -// Extension to EpisodeMetadata for integration with the new manager -extension EpisodeMetadata { - func toData() -> Data? { - // Convert to EpisodeMetadataInfo first - let info = EpisodeMetadataInfo( - title: self.title, - imageUrl: self.imageUrl, - anilistId: self.anilistId, - episodeNumber: self.episodeNumber - ) - - // Then encode to Data - return try? JSONEncoder().encode(info) - } - - static func fromData(_ data: Data) -> EpisodeMetadata? { - guard let info = try? JSONDecoder().decode(EpisodeMetadataInfo.self, from: data) else { - return nil - } - - return EpisodeMetadata( - title: info.title, - imageUrl: info.imageUrl, - anilistId: info.anilistId, - episodeNumber: info.episodeNumber - ) - } -} \ No newline at end of file diff --git a/Sora/Managers/ImagePrefetchManager.swift b/Sora/Managers/ImagePrefetchManager.swift deleted file mode 100644 index a4d342c..0000000 --- a/Sora/Managers/ImagePrefetchManager.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// ImagePrefetchManager.swift -// Sora -// -// Created by doomsboygaming on 5/22/25 -// - -import Foundation -import Kingfisher -import UIKit - -/// Manager for image prefetching, caching, and optimization -class ImagePrefetchManager { - static let shared = ImagePrefetchManager() - - // Prefetcher for batch prefetching images - private let prefetcher = ImagePrefetcher( - urls: [], - options: [ - .processor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))), - .scaleFactor(UIScreen.main.scale), - .cacheOriginalImage - ] - ) - - // Keep track of what's already prefetched to avoid duplication - private var prefetchedURLs = Set() - private let prefetchQueue = DispatchQueue(label: "com.sora.imagePrefetch", qos: .utility) - - init() { - // Set up KingfisherManager for optimal image loading - ImageCache.default.memoryStorage.config.totalCostLimit = 300 * 1024 * 1024 // 300MB - ImageCache.default.diskStorage.config.sizeLimit = 1000 * 1024 * 1024 // 1GB - ImageDownloader.default.downloadTimeout = 15.0 // 15 seconds - } - - /// Prefetch a batch of images - func prefetchImages(_ urls: [String]) { - prefetchQueue.async { [weak self] in - guard let self = self else { return } - - // Filter out already prefetched URLs and invalid URLs - let urlObjects = urls.compactMap { URL(string: $0) } - .filter { !self.prefetchedURLs.contains($0) } - - guard !urlObjects.isEmpty else { return } - - // Create a new prefetcher with the URLs and start it - let newPrefetcher = ImagePrefetcher( - urls: urlObjects, - options: [ - .processor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))), - .scaleFactor(UIScreen.main.scale), - .cacheOriginalImage - ] - ) - newPrefetcher.start() - - // Track prefetched URLs - urlObjects.forEach { self.prefetchedURLs.insert($0) } - } - } - - /// Prefetch a single image - func prefetchImage(_ url: String) { - guard let urlObject = URL(string: url), - !prefetchedURLs.contains(urlObject) else { - return - } - - prefetchQueue.async { [weak self] in - guard let self = self else { return } - - // Create a new prefetcher with the URL and start it - let newPrefetcher = ImagePrefetcher( - urls: [urlObject], - options: [ - .processor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))), - .scaleFactor(UIScreen.main.scale), - .cacheOriginalImage - ] - ) - newPrefetcher.start() - - // Track prefetched URL - self.prefetchedURLs.insert(urlObject) - } - } - - /// Prefetch episode images for a batch of episodes - func prefetchEpisodeImages(anilistId: Int, startEpisode: Int, count: Int) { - prefetchQueue.async { [weak self] in - guard let self = self else { return } - - // Get metadata for episodes in the range - for episodeNumber in startEpisode...(startEpisode + count) where episodeNumber > 0 { - EpisodeMetadataManager.shared.fetchMetadata(anilistId: anilistId, episodeNumber: episodeNumber) { result in - switch result { - case .success(let metadata): - self.prefetchImage(metadata.imageUrl) - case .failure: - break - } - } - } - } - } - - /// Clear prefetch queue and stop any ongoing prefetch operations - func cancelPrefetching() { - prefetcher.stop() - } -} - -// MARK: - KFImage Extension -extension KFImage { - /// Load an image with optimal settings for episode thumbnails - static func optimizedEpisodeThumbnail(url: URL?) -> KFImage { - return KFImage(url) - .setProcessor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))) - .memoryCacheExpiration(.seconds(300)) - .cacheOriginalImage() - .fade(duration: 0.25) - .onProgress { _, _ in - // Track progress if needed - } - .onSuccess { _ in - // Success logger removed to reduce logs - } - .onFailure { error in - Logger.shared.log("Failed to load image: \(error)", type: "Error") - } - } -} \ No newline at end of file diff --git a/Sora/Managers/ImageUpscaler.swift b/Sora/Managers/ImageUpscaler.swift deleted file mode 100644 index 70643d8..0000000 --- a/Sora/Managers/ImageUpscaler.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// ImageUpscaler.swift -// Sulfur -// -// Created by seiike on 26/05/2025. -// - - -import UIKit -import CoreImage -import CoreImage.CIFilterBuiltins -import Vision -import CoreML -import Kingfisher - -public enum ImageUpscaler { - /// Lanczos interpolation + unsharp mask for sharper upscaling. - /// - Parameters: - /// - scale: The factor to upscale (e.g. 2.0 doubles width/height). - /// - sharpeningIntensity: The unsharp mask intensity (0...1). - /// - sharpeningRadius: The unsharp mask radius in pixels. - public static func lanczosProcessor( - scale: CGFloat, - sharpeningIntensity: Float = 0.7, - sharpeningRadius: Float = 2.0 - ) -> ImageProcessor { - return LanczosUpscaleProcessor( - scale: scale, - sharpeningIntensity: sharpeningIntensity, - sharpeningRadius: sharpeningRadius - ) - } - - public static func superResolutionProcessor(modelURL: URL) -> ImageProcessor { - return MLScaleProcessor(modelURL: modelURL) - } -} - -// MARK: - Lanczos + Unsharp Mask Processor -public struct LanczosUpscaleProcessor: ImageProcessor { - public let scale: CGFloat - public let sharpeningIntensity: Float - public let sharpeningRadius: Float - public var identifier: String { - "com.yourapp.lanczos_\(scale)_sharp_\(sharpeningIntensity)_\(sharpeningRadius)" - } - - public init( - scale: CGFloat, - sharpeningIntensity: Float = 0.7, - sharpeningRadius: Float = 2.0 - ) { - self.scale = scale - self.sharpeningIntensity = sharpeningIntensity - self.sharpeningRadius = sharpeningRadius - } - - public func process( - item: ImageProcessItem, - options: KingfisherParsedOptionsInfo - ) -> KFCrossPlatformImage? { - - let inputImage: KFCrossPlatformImage? - switch item { - case .image(let image): - inputImage = image - case .data(let data): - inputImage = KFCrossPlatformImage(data: data) - } - guard let uiImage = inputImage, - let cgImage = uiImage.cgImage else { - return nil - } - - let ciInput = CIImage(cgImage: cgImage) - - let scaleFilter = CIFilter.lanczosScaleTransform() - scaleFilter.inputImage = ciInput - scaleFilter.scale = Float(scale) - scaleFilter.aspectRatio = 1.0 - guard let scaledCI = scaleFilter.outputImage else { - return uiImage - } - - let unsharp = CIFilter.unsharpMask() - unsharp.inputImage = scaledCI - unsharp.intensity = sharpeningIntensity - unsharp.radius = sharpeningRadius - guard let sharpCI = unsharp.outputImage else { - return UIImage(ciImage: scaledCI) - } - - let context = CIContext(options: nil) - guard let outputCG = context.createCGImage(sharpCI, from: sharpCI.extent) else { - return UIImage(ciImage: sharpCI) - } - return KFCrossPlatformImage(cgImage: outputCG) - } -} - -// MARK: - Core ML Super-Resolution Processor -public struct MLScaleProcessor: ImageProcessor { - private let request: VNCoreMLRequest - private let ciContext = CIContext() - public let identifier: String - - public init(modelURL: URL) { - - self.identifier = "com.yourapp.ml_sr_\(modelURL.lastPathComponent)" - guard let mlModel = try? MLModel(contentsOf: modelURL), - let visionModel = try? VNCoreMLModel(for: mlModel) else { - fatalError("Failed to load Core ML model at \(modelURL)") - } - let req = VNCoreMLRequest(model: visionModel) - req.imageCropAndScaleOption = .scaleFill - self.request = req - } - - public func process( - item: ImageProcessItem, - options: KingfisherParsedOptionsInfo - ) -> KFCrossPlatformImage? { - - let inputImage: KFCrossPlatformImage? - switch item { - case .image(let image): - inputImage = image - case .data(let data): - inputImage = KFCrossPlatformImage(data: data) - } - guard let uiImage = inputImage, - let cgImage = uiImage.cgImage else { - return nil - } - - let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) - do { - try handler.perform([request]) - } catch { - print("[MLScaleProcessor] Vision error: \(error)") - return uiImage - } - guard let obs = request.results?.first as? VNPixelBufferObservation else { - return uiImage - } - - let ciOutput = CIImage(cvPixelBuffer: obs.pixelBuffer) - let rect = CGRect( - origin: .zero, - size: CGSize( - width: CVPixelBufferGetWidth(obs.pixelBuffer), - height: CVPixelBufferGetHeight(obs.pixelBuffer) - ) - ) - guard let finalCG = ciContext.createCGImage(ciOutput, from: rect) else { - return uiImage - } - return KFCrossPlatformImage(cgImage: finalCG) - } -} - -// the sweet spot (for mediainfoview poster) -// .setProcessor(ImageUpscaler.lanczosProcessor(scale: 3.2, -// sharpeningIntensity: 0.75, -// sharpeningRadius: 2.25)) diff --git a/Sora/Managers/PerformanceMonitor.swift b/Sora/Managers/PerformanceMonitor.swift deleted file mode 100644 index 3a45fae..0000000 --- a/Sora/Managers/PerformanceMonitor.swift +++ /dev/null @@ -1,510 +0,0 @@ -// -// PerformanceMonitor.swift -// Sora -// -// Created by doomsboygaming on 5/22/25 -// - -import Foundation -import SwiftUI -import Kingfisher -import QuartzCore - -/// Performance metrics tracking system with advanced jitter detection -class PerformanceMonitor: ObservableObject { - static let shared = PerformanceMonitor() - - // Published properties to allow UI observation - @Published private(set) var networkRequestCount: Int = 0 - @Published private(set) var cacheHitCount: Int = 0 - @Published private(set) var cacheMissCount: Int = 0 - @Published private(set) var averageLoadTime: TimeInterval = 0 - @Published private(set) var memoryUsage: UInt64 = 0 - @Published private(set) var diskUsage: UInt64 = 0 - @Published private(set) var isEnabled: Bool = false - - // Advanced performance metrics for jitter detection - @Published private(set) var currentFPS: Double = 60.0 - @Published private(set) var mainThreadBlocks: Int = 0 - @Published private(set) var memorySpikes: Int = 0 - @Published private(set) var cpuUsage: Double = 0.0 - @Published private(set) var jitterEvents: Int = 0 - - // Internal tracking properties - private var loadTimes: [TimeInterval] = [] - private var startTimes: [String: Date] = [:] - private var memoryTimer: Timer? - private var logTimer: Timer? - - // Advanced monitoring properties - private var displayLink: CADisplayLink? - private var frameCount: Int = 0 - private var lastFrameTime: CFTimeInterval = 0 - private var frameTimes: [CFTimeInterval] = [] - private var lastMemoryUsage: UInt64 = 0 - private var mainThreadOperations: [String: CFTimeInterval] = [:] - private var cpuTimer: Timer? - - // Thresholds for performance issues - private let mainThreadBlockingThreshold: TimeInterval = 0.016 // 16ms for 60fps - private let memorySpikeTreshold: UInt64 = 50 * 1024 * 1024 // 50MB spike - private let fpsThreshold: Double = 50.0 // Below 50fps is considered poor - - private init() { - // Default is off unless explicitly enabled - isEnabled = UserDefaults.standard.bool(forKey: "enablePerformanceMonitoring") - - // Setup memory monitoring if enabled - if isEnabled { - startMonitoring() - } - } - - // MARK: - Public Methods - - /// Enable or disable the performance monitoring - func setEnabled(_ enabled: Bool) { - isEnabled = enabled - UserDefaults.standard.set(enabled, forKey: "enablePerformanceMonitoring") - - if enabled { - startMonitoring() - } else { - stopMonitoring() - } - } - - /// Reset all tracked metrics - func resetMetrics() { - networkRequestCount = 0 - cacheHitCount = 0 - cacheMissCount = 0 - averageLoadTime = 0 - loadTimes = [] - startTimes = [:] - - // Reset advanced metrics - mainThreadBlocks = 0 - memorySpikes = 0 - jitterEvents = 0 - frameTimes = [] - frameCount = 0 - mainThreadOperations = [:] - - updateMemoryUsage() - - Logger.shared.log("Performance metrics reset", type: "Debug") - } - - /// Track a network request starting - func trackRequestStart(identifier: String) { - guard isEnabled else { return } - - networkRequestCount += 1 - startTimes[identifier] = Date() - } - - /// Track a network request completing - func trackRequestEnd(identifier: String) { - guard isEnabled, let startTime = startTimes[identifier] else { return } - - let endTime = Date() - let duration = endTime.timeIntervalSince(startTime) - loadTimes.append(duration) - - // Update average load time - if !loadTimes.isEmpty { - averageLoadTime = loadTimes.reduce(0, +) / Double(loadTimes.count) - } - - // Remove start time to avoid memory leaks - startTimes.removeValue(forKey: identifier) - } - - /// Track a cache hit - func trackCacheHit() { - guard isEnabled else { return } - cacheHitCount += 1 - } - - /// Track a cache miss - func trackCacheMiss() { - guard isEnabled else { return } - cacheMissCount += 1 - } - - // MARK: - Advanced Performance Monitoring - - /// Track the start of a main thread operation - func trackMainThreadOperationStart(operation: String) { - guard isEnabled else { return } - mainThreadOperations[operation] = CACurrentMediaTime() - } - - /// Track the end of a main thread operation and detect blocking - func trackMainThreadOperationEnd(operation: String) { - guard isEnabled, let startTime = mainThreadOperations[operation] else { return } - - let endTime = CACurrentMediaTime() - let duration = endTime - startTime - - if duration > mainThreadBlockingThreshold { - mainThreadBlocks += 1 - jitterEvents += 1 - - let durationMs = Int(duration * 1000) - Logger.shared.log("🚨 Main thread blocked for \(durationMs)ms during: \(operation)", type: "Performance") - } - - mainThreadOperations.removeValue(forKey: operation) - } - - /// Track memory spikes during downloads - func checkMemorySpike() { - guard isEnabled else { return } - - let currentMemory = getAppMemoryUsage() - - if lastMemoryUsage > 0 { - let spike = currentMemory > lastMemoryUsage ? currentMemory - lastMemoryUsage : 0 - - if spike > memorySpikeTreshold { - memorySpikes += 1 - jitterEvents += 1 - - let spikeSize = Double(spike) / (1024 * 1024) - Logger.shared.log("🚨 Memory spike detected: +\(String(format: "%.1f", spikeSize))MB", type: "Performance") - } - } - - lastMemoryUsage = currentMemory - memoryUsage = currentMemory - } - - /// Start frame rate monitoring - private func startFrameRateMonitoring() { - guard displayLink == nil else { return } - - displayLink = CADisplayLink(target: self, selector: #selector(frameCallback)) - displayLink?.add(to: .main, forMode: .common) - - frameCount = 0 - lastFrameTime = CACurrentMediaTime() - frameTimes = [] - } - - /// Stop frame rate monitoring - private func stopFrameRateMonitoring() { - displayLink?.invalidate() - displayLink = nil - } - - /// Frame callback for FPS monitoring - @objc private func frameCallback() { - let currentTime = CACurrentMediaTime() - - if lastFrameTime > 0 { - let frameDuration = currentTime - lastFrameTime - frameTimes.append(frameDuration) - - // Keep only last 60 frames for rolling average - if frameTimes.count > 60 { - frameTimes.removeFirst() - } - - // Calculate current FPS - if !frameTimes.isEmpty { - let averageFrameTime = frameTimes.reduce(0, +) / Double(frameTimes.count) - currentFPS = 1.0 / averageFrameTime - - // Detect FPS drops - if currentFPS < fpsThreshold { - jitterEvents += 1 - Logger.shared.log("🚨 FPS drop detected: \(String(format: "%.1f", currentFPS))fps", type: "Performance") - } - } - } - - lastFrameTime = currentTime - frameCount += 1 - } - - /// Get current CPU usage - private func getCPUUsage() -> Double { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size) / 4 - - let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { - task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count) - } - } - - if kerr == KERN_SUCCESS { - // This is a simplified CPU usage calculation - // For more accurate results, we'd need to track over time - return Double(info.user_time.seconds + info.system_time.seconds) - } else { - return 0.0 - } - } - - /// Get the current cache hit rate - var cacheHitRate: Double { - let total = cacheHitCount + cacheMissCount - guard total > 0 else { return 0 } - return Double(cacheHitCount) / Double(total) - } - - /// Log current performance metrics - func logMetrics() { - guard isEnabled else { return } - - checkMemorySpike() - - let hitRate = String(format: "%.1f%%", cacheHitRate * 100) - let avgLoad = String(format: "%.2f", averageLoadTime) - let memory = String(format: "%.1f MB", Double(memoryUsage) / (1024 * 1024)) - let disk = String(format: "%.1f MB", Double(diskUsage) / (1024 * 1024)) - let fps = String(format: "%.1f", currentFPS) - let cpu = String(format: "%.1f%%", cpuUsage) - - let metrics = """ - 📊 Performance Metrics Report: - ═══════════════════════════════ - Network & Cache: - - Network Requests: \(networkRequestCount) - - Cache Hit Rate: \(hitRate) (\(cacheHitCount)/\(cacheHitCount + cacheMissCount)) - - Average Load Time: \(avgLoad)s - - System Resources: - - Memory Usage: \(memory) - - Disk Usage: \(disk) - - CPU Usage: \(cpu) - - Performance Issues: - - Current FPS: \(fps) - - Main Thread Blocks: \(mainThreadBlocks) - - Memory Spikes: \(memorySpikes) - - Total Jitter Events: \(jitterEvents) - ═══════════════════════════════ - """ - - Logger.shared.log(metrics, type: "Performance") - - // Alert if performance is poor - if jitterEvents > 0 { - Logger.shared.log("⚠️ Performance issues detected! Check logs above for details.", type: "Warning") - } - } - - // MARK: - Private Methods - - private func startMonitoring() { - // Setup timer to update memory usage periodically and check for spikes - memoryTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in - self?.checkMemorySpike() - } - - // Setup timer to log metrics periodically - logTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in - self?.logMetrics() - } - - // Setup CPU monitoring timer - cpuTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in - self?.cpuUsage = self?.getCPUUsage() ?? 0.0 - } - - // Make sure timers run even when scrolling - RunLoop.current.add(memoryTimer!, forMode: .common) - RunLoop.current.add(logTimer!, forMode: .common) - RunLoop.current.add(cpuTimer!, forMode: .common) - - // Start frame rate monitoring - startFrameRateMonitoring() - - Logger.shared.log("Advanced performance monitoring started - tracking FPS, main thread blocks, memory spikes", type: "Debug") - } - - private func stopMonitoring() { - memoryTimer?.invalidate() - memoryTimer = nil - - logTimer?.invalidate() - logTimer = nil - - cpuTimer?.invalidate() - cpuTimer = nil - - stopFrameRateMonitoring() - - Logger.shared.log("Performance monitoring stopped", type: "Debug") - } - - private func updateMemoryUsage() { - memoryUsage = getAppMemoryUsage() - diskUsage = getCacheDiskUsage() - } - - private func getAppMemoryUsage() -> UInt64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size) / 4 - - let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { - task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count) - } - } - - if kerr == KERN_SUCCESS { - return info.resident_size - } else { - return 0 - } - } - - private func getCacheDiskUsage() -> UInt64 { - // Try to get Kingfisher's disk cache size - let diskCache = ImageCache.default.diskStorage - - do { - let size = try diskCache.totalSize() - return UInt64(size) - } catch { - Logger.shared.log("Failed to get disk cache size: \(error)", type: "Error") - return 0 - } - } -} - -// MARK: - Extensions to integrate with managers - -extension EpisodeMetadataManager { - /// Integrate performance tracking - func trackFetchStart(anilistId: Int, episodeNumber: Int) { - let identifier = "metadata_\(anilistId)_\(episodeNumber)" - PerformanceMonitor.shared.trackRequestStart(identifier: identifier) - } - - func trackFetchEnd(anilistId: Int, episodeNumber: Int) { - let identifier = "metadata_\(anilistId)_\(episodeNumber)" - PerformanceMonitor.shared.trackRequestEnd(identifier: identifier) - } - - func trackCacheHit() { - PerformanceMonitor.shared.trackCacheHit() - } - - func trackCacheMiss() { - PerformanceMonitor.shared.trackCacheMiss() - } -} - -extension ImagePrefetchManager { - /// Integrate performance tracking - func trackImageLoadStart(url: String) { - let identifier = "image_\(url.hashValue)" - PerformanceMonitor.shared.trackRequestStart(identifier: identifier) - } - - func trackImageLoadEnd(url: String) { - let identifier = "image_\(url.hashValue)" - PerformanceMonitor.shared.trackRequestEnd(identifier: identifier) - } - - func trackImageCacheHit() { - PerformanceMonitor.shared.trackCacheHit() - } - - func trackImageCacheMiss() { - PerformanceMonitor.shared.trackCacheMiss() - } -} - -// MARK: - Debug View -struct PerformanceMetricsView: View { - @ObservedObject private var monitor = PerformanceMonitor.shared - @State private var isExpanded = false - - var body: some View { - VStack { - HStack { - Text("Performance Metrics") - .font(.headline) - - Spacer() - - Button(action: { - isExpanded.toggle() - }) { - Image(systemName: isExpanded ? "chevron.up" : "chevron.down") - } - } - .padding(.horizontal) - - if isExpanded { - VStack(alignment: .leading, spacing: 4) { - Text("Network Requests: \(monitor.networkRequestCount)") - Text("Cache Hit Rate: \(Int(monitor.cacheHitRate * 100))%") - Text("Avg Load Time: \(String(format: "%.2f", monitor.averageLoadTime))s") - Text("Memory: \(String(format: "%.1f MB", Double(monitor.memoryUsage) / (1024 * 1024)))") - - Divider() - - // Advanced metrics - Text("FPS: \(String(format: "%.1f", monitor.currentFPS))") - .foregroundColor(monitor.currentFPS < 50 ? .red : .primary) - Text("Main Thread Blocks: \(monitor.mainThreadBlocks)") - .foregroundColor(monitor.mainThreadBlocks > 0 ? .red : .primary) - Text("Memory Spikes: \(monitor.memorySpikes)") - .foregroundColor(monitor.memorySpikes > 0 ? .orange : .primary) - Text("Jitter Events: \(monitor.jitterEvents)") - .foregroundColor(monitor.jitterEvents > 0 ? .red : .primary) - - HStack { - Button(action: { - monitor.resetMetrics() - }) { - Text("Reset") - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(4) - } - - Button(action: { - monitor.logMetrics() - }) { - Text("Log") - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.green) - .foregroundColor(.white) - .cornerRadius(4) - } - - Toggle("", isOn: Binding( - get: { monitor.isEnabled }, - set: { monitor.setEnabled($0) } - )) - .labelsHidden() - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal) - .padding(.bottom, 8) - .background(Color.secondary.opacity(0.1)) - .cornerRadius(8) - .padding(.horizontal) - } - } - .padding(.vertical, 4) - .background(Color.secondary.opacity(0.05)) - .cornerRadius(8) - .padding(8) - } -} \ No newline at end of file diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 327720e..79f8db0 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -58,9 +58,6 @@ struct SoraApp: App { @StateObject private var jsController = JSController.shared init() { - _ = MetadataCacheManager.shared - _ = KingfisherCacheManager.shared - if let userAccentColor = UserDefaults.standard.color(forKey: "accentColor") { UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = userAccentColor } diff --git a/Sora/Utils/Cache/EpisodeMetadata.swift b/Sora/Utils/Cache/EpisodeMetadata.swift deleted file mode 100644 index c27e25a..0000000 --- a/Sora/Utils/Cache/EpisodeMetadata.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// EpisodeMetadata.swift -// Sora -// -// Created by doomsboygaming on 5/22/25 -// - -import Foundation - -/// Represents metadata for an episode, used for caching -struct EpisodeMetadata: Codable { - /// Title of the episode - let title: [String: String] - - /// Image URL for the episode - let imageUrl: String - - /// AniList ID of the show - let anilistId: Int - - /// Episode number - let episodeNumber: Int - - /// When this metadata was cached - let cacheDate: Date - - /// Unique cache key for this episode metadata - var cacheKey: String { - return "anilist_\(anilistId)_episode_\(episodeNumber)" - } - - /// Initialize with the basic required data - /// - Parameters: - /// - title: Dictionary of titles by language code - /// - imageUrl: URL of the episode thumbnail image - /// - anilistId: ID of the show in AniList - /// - episodeNumber: Number of the episode - init(title: [String: String], imageUrl: String, anilistId: Int, episodeNumber: Int) { - self.title = title - self.imageUrl = imageUrl - self.anilistId = anilistId - self.episodeNumber = episodeNumber - self.cacheDate = Date() - } -} \ No newline at end of file diff --git a/Sora/Utils/Cache/KingfisherManager.swift b/Sora/Utils/Cache/KingfisherManager.swift deleted file mode 100644 index 860d02c..0000000 --- a/Sora/Utils/Cache/KingfisherManager.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// KingfisherManager.swift -// Sora -// -// Created by doomsboygaming on 5/22/25 -// - -import Foundation -import Kingfisher -import SwiftUI - -/// Manages Kingfisher image caching configuration -class KingfisherCacheManager { - static let shared = KingfisherCacheManager() - - /// Maximum disk cache size (default 500MB) - private let maxDiskCacheSize: UInt = 500 * 1024 * 1024 - - /// Maximum cache age (default 7 days) - private let maxCacheAgeInDays: TimeInterval = 7 - - /// UserDefaults keys - private let imageCachingEnabledKey = "imageCachingEnabled" - - /// Whether image caching is enabled - var isCachingEnabled: Bool { - get { - // Default to true if not set - UserDefaults.standard.object(forKey: imageCachingEnabledKey) == nil ? - true : UserDefaults.standard.bool(forKey: imageCachingEnabledKey) - } - set { - UserDefaults.standard.set(newValue, forKey: imageCachingEnabledKey) - configureKingfisher() - } - } - - private init() { - configureKingfisher() - } - - /// Configure Kingfisher with appropriate caching settings - func configureKingfisher() { - let cache = ImageCache.default - - // Set disk cache size limit and expiration - cache.diskStorage.config.sizeLimit = isCachingEnabled ? maxDiskCacheSize : 0 - cache.diskStorage.config.expiration = isCachingEnabled ? - .days(Int(maxCacheAgeInDays)) : .seconds(1) // 1 second means effectively disabled - - // Set memory cache size - cache.memoryStorage.config.totalCostLimit = isCachingEnabled ? - 30 * 1024 * 1024 : 0 // 30MB memory cache when enabled - - // Configure clean interval - cache.memoryStorage.config.cleanInterval = 60 // Clean memory every 60 seconds - - // Configure retry strategy - KingfisherManager.shared.downloader.downloadTimeout = 15.0 // 15 second timeout - - Logger.shared.log("Configured Kingfisher cache. Enabled: \(isCachingEnabled)", type: "Debug") - } - - /// Clear all cached images - func clearCache(completion: (() -> Void)? = nil) { - KingfisherManager.shared.cache.clearMemoryCache() - KingfisherManager.shared.cache.clearDiskCache { - Logger.shared.log("Cleared Kingfisher image cache", type: "General") - completion?() - } - } - - /// Calculate current cache size - /// - Parameter completion: Closure to call with cache size in bytes - func calculateCacheSize(completion: @escaping (UInt) -> Void) { - KingfisherManager.shared.cache.calculateDiskStorageSize { result in - switch result { - case .success(let size): - completion(size) - case .failure(let error): - Logger.shared.log("Failed to calculate image cache size: \(error)", type: "Error") - completion(0) - } - } - } - - /// Convert cache size to user-friendly string - /// - Parameter sizeInBytes: Size in bytes - /// - Returns: Formatted string (e.g., "5.2 MB") - static func formatCacheSize(_ sizeInBytes: UInt) -> String { - let formatter = ByteCountFormatter() - formatter.countStyle = .file - return formatter.string(fromByteCount: Int64(sizeInBytes)) - } -} \ No newline at end of file diff --git a/Sora/Utils/Cache/MetadataCacheManager.swift b/Sora/Utils/Cache/MetadataCacheManager.swift deleted file mode 100644 index aa1280b..0000000 --- a/Sora/Utils/Cache/MetadataCacheManager.swift +++ /dev/null @@ -1,276 +0,0 @@ -// -// MetadataCacheManager.swift -// Sora -// -// Created by doomsboygaming on 5/22/25 -// - -import Foundation -import SwiftUI - -/// A class to manage episode metadata caching, both in-memory and on disk -class MetadataCacheManager { - static let shared = MetadataCacheManager() - - // In-memory cache - private let memoryCache = NSCache() - - // File manager for disk operations - private let fileManager = FileManager.default - - // Cache directory URL - private var cacheDirectory: URL - - // Cache expiration - 7 days by default - private let maxCacheAge: TimeInterval = 7 * 24 * 60 * 60 - - // UserDefaults keys - private let metadataCachingEnabledKey = "metadataCachingEnabled" - private let memoryOnlyModeKey = "metadataMemoryOnlyCache" - private let lastCacheCleanupKey = "lastMetadataCacheCleanup" - - // Analytics counters - private(set) var cacheHits: Int = 0 - private(set) var cacheMisses: Int = 0 - - // MARK: - Public properties - - /// Whether metadata caching is enabled (persisted in UserDefaults) - var isCachingEnabled: Bool { - get { - // Default to true if not set - UserDefaults.standard.object(forKey: metadataCachingEnabledKey) == nil ? - true : UserDefaults.standard.bool(forKey: metadataCachingEnabledKey) - } - set { - UserDefaults.standard.set(newValue, forKey: metadataCachingEnabledKey) - } - } - - /// Whether to use memory-only mode (no disk caching) - var isMemoryOnlyMode: Bool { - get { - UserDefaults.standard.bool(forKey: memoryOnlyModeKey) - } - set { - UserDefaults.standard.set(newValue, forKey: memoryOnlyModeKey) - } - } - - // MARK: - Initialization - - private init() { - // Set up cache directory - do { - let cachesDirectory = try fileManager.url( - for: .cachesDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) - cacheDirectory = cachesDirectory.appendingPathComponent("EpisodeMetadata", isDirectory: true) - - // Create the directory if it doesn't exist - if !fileManager.fileExists(atPath: cacheDirectory.path) { - try fileManager.createDirectory(at: cacheDirectory, - withIntermediateDirectories: true, - attributes: nil) - } - - // Set up memory cache - memoryCache.name = "EpisodeMetadataCache" - memoryCache.countLimit = 100 // Limit number of items in memory - - // Clean up old files if needed - cleanupOldCacheFilesIfNeeded() - - } catch { - Logger.shared.log("Failed to set up metadata cache directory: \(error)", type: "Error") - // Fallback to temporary directory - cacheDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("EpisodeMetadata") - } - } - - // MARK: - Public Methods - - /// Store metadata in the cache - /// - Parameters: - /// - data: The metadata to cache - /// - key: The cache key (usually anilist_id + episode_number) - func storeMetadata(_ data: Data, forKey key: String) { - guard isCachingEnabled else { return } - - let keyString = key as NSString - - // Always store in memory cache - memoryCache.setObject(data as NSData, forKey: keyString) - - // Store on disk if not in memory-only mode - if !isMemoryOnlyMode { - let fileURL = cacheDirectory.appendingPathComponent(key) - - DispatchQueue.global(qos: .background).async { [weak self] in - do { - try data.write(to: fileURL) - - // Add timestamp as a file attribute instead of using extended attributes - let attributes: [FileAttributeKey: Any] = [ - .creationDate: Date() - ] - try self?.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path) - - Logger.shared.log("Metadata cached for key: \(key)", type: "Debug") - } catch { - Logger.shared.log("Failed to write metadata to disk: \(error)", type: "Error") - } - } - } - } - - /// Retrieve metadata from cache - /// - Parameter key: The cache key - /// - Returns: The cached metadata if available and not expired, nil otherwise - func getMetadata(forKey key: String) -> Data? { - guard isCachingEnabled else { - return nil - } - - let keyString = key as NSString - - // Try memory cache first - if let cachedData = memoryCache.object(forKey: keyString) as Data? { - return cachedData - } - - // If not in memory and not in memory-only mode, try disk - if !isMemoryOnlyMode { - let fileURL = cacheDirectory.appendingPathComponent(key) - - do { - // Check if file exists - if fileManager.fileExists(atPath: fileURL.path) { - // Check if the file is not expired - if !isFileExpired(at: fileURL) { - let data = try Data(contentsOf: fileURL) - - // Store in memory cache for faster access next time - memoryCache.setObject(data as NSData, forKey: keyString) - - return data - } else { - // File is expired, remove it - try fileManager.removeItem(at: fileURL) - } - } - } catch { - Logger.shared.log("Error accessing disk cache: \(error)", type: "Error") - } - } - - return nil - } - - /// Clear all cached metadata - func clearAllCache() { - // Clear memory cache - memoryCache.removeAllObjects() - - // Clear disk cache - if !isMemoryOnlyMode { - do { - let fileURLs = try fileManager.contentsOfDirectory(at: cacheDirectory, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - - for fileURL in fileURLs { - try fileManager.removeItem(at: fileURL) - } - - Logger.shared.log("Cleared all metadata cache", type: "General") - } catch { - Logger.shared.log("Failed to clear disk cache: \(error)", type: "Error") - } - } - - // Reset analytics - cacheHits = 0 - cacheMisses = 0 - } - - /// Clear expired cache entries - func clearExpiredCache() { - guard !isMemoryOnlyMode else { return } - - do { - let fileURLs = try fileManager.contentsOfDirectory(at: cacheDirectory, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - - var removedCount = 0 - - for fileURL in fileURLs { - if isFileExpired(at: fileURL) { - try fileManager.removeItem(at: fileURL) - removedCount += 1 - } - } - - if removedCount > 0 { - Logger.shared.log("Cleared \(removedCount) expired metadata cache items", type: "General") - } - } catch { - Logger.shared.log("Failed to clear expired cache: \(error)", type: "Error") - } - } - - /// Get the total size of the cache on disk - /// - Returns: Size in bytes - func getCacheSize() -> Int64 { - guard !isMemoryOnlyMode else { return 0 } - - do { - let fileURLs = try fileManager.contentsOfDirectory(at: cacheDirectory, - includingPropertiesForKeys: [.fileSizeKey], - options: .skipsHiddenFiles) - - return fileURLs.reduce(0) { result, url in - do { - let attributes = try url.resourceValues(forKeys: [.fileSizeKey]) - return result + Int64(attributes.fileSize ?? 0) - } catch { - return result - } - } - } catch { - Logger.shared.log("Failed to calculate cache size: \(error)", type: "Error") - return 0 - } - } - - // MARK: - Private Helper Methods - - private func isFileExpired(at url: URL) -> Bool { - do { - let attributes = try fileManager.attributesOfItem(atPath: url.path) - if let creationDate = attributes[.creationDate] as? Date { - return Date().timeIntervalSince(creationDate) > maxCacheAge - } - return true // If can't determine age, consider it expired - } catch { - return true // If error reading attributes, consider it expired - } - } - - private func cleanupOldCacheFilesIfNeeded() { - // Only run cleanup once a day - let lastCleanupTime = UserDefaults.standard.double(forKey: lastCacheCleanupKey) - let dayInSeconds: TimeInterval = 24 * 60 * 60 - - if Date().timeIntervalSince1970 - lastCleanupTime > dayInSeconds { - DispatchQueue.global(qos: .background).async { [weak self] in - self?.clearExpiredCache() - UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: self?.lastCacheCleanupKey ?? "") - } - } - } -} \ No newline at end of file diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index 3130c60..dd8c20b 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -268,7 +268,6 @@ struct EpisodeCell: View { .onFailure { error in Logger.shared.log("Failed to load episode image: \(error)", type: "Error") } - .cacheMemoryOnly(!KingfisherCacheManager.shared.isCachingEnabled) .resizable() .aspectRatio(16/9, contentMode: .fill) .frame(width: 100, height: 56) @@ -713,25 +712,6 @@ struct EpisodeCell: View { } private func fetchEpisodeDetails() { - if MetadataCacheManager.shared.isCachingEnabled && - (UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil || - UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata")) { - - let cacheKey = "anilist_\(itemID)_episode_\(episodeID + 1)" - - if let cachedData = MetadataCacheManager.shared.getMetadata(forKey: cacheKey), - let metadata = EpisodeMetadata.fromData(cachedData) { - - DispatchQueue.main.async { - self.episodeTitle = metadata.title["en"] ?? "" - self.episodeImageUrl = metadata.imageUrl - self.isLoading = false - self.loadedFromCache = true - } - return - } - } - fetchAnimeEpisodeDetails() } @@ -808,22 +788,6 @@ struct EpisodeCell: View { Logger.shared.log("Episode \(episodeKey) missing fields: \(missingFields.joined(separator: ", "))", type: "Warning") } - if MetadataCacheManager.shared.isCachingEnabled && (!title.isEmpty || !image.isEmpty) { - let metadata = EpisodeMetadata( - title: title, - imageUrl: image, - anilistId: self.itemID, - episodeNumber: self.episodeID + 1 - ) - - if let metadataData = metadata.toData() { - MetadataCacheManager.shared.storeMetadata( - metadataData, - forKey: metadata.cacheKey - ) - } - } - DispatchQueue.main.async { self.isLoading = false self.retryAttempts = 0 diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index eea826e..7a971b1 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -220,14 +220,12 @@ struct MediaInfoView: View { .fill(Color.gray.opacity(0.3)) .shimmering() } - .setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1.5, sharpeningRadius: 0.8)) .resizable() .aspectRatio(contentMode: .fill) .frame(width: UIScreen.main.bounds.width, height: 700) .clipped() KFImage(URL(string: imageUrl)) .placeholder { EmptyView() } - .setProcessor(ImageUpscaler.lanczosProcessor(scale: 3, sharpeningIntensity: 1.5, sharpeningRadius: 0.8)) .resizable() .aspectRatio(contentMode: .fill) .frame(width: UIScreen.main.bounds.width, height: 700) @@ -1863,24 +1861,6 @@ struct MediaInfoView: View { return } - if MetadataCacheManager.shared.isCachingEnabled { - let cacheKey = "anilist_\(anilistId)_episode_\(episode.number)" - - if let cachedData = MetadataCacheManager.shared.getMetadata(forKey: cacheKey), - let metadata = EpisodeMetadata.fromData(cachedData) { - - print("[Bulk Download] Using cached metadata for episode \(episode.number)") - let metadataInfo = EpisodeMetadataInfo( - title: metadata.title, - imageUrl: metadata.imageUrl, - anilistId: metadata.anilistId, - episodeNumber: metadata.episodeNumber - ) - completion(metadataInfo) - return - } - } - fetchEpisodeMetadataFromNetwork(anilistId: anilistId, episodeNumber: episode.number, completion: completion) } @@ -1939,22 +1919,6 @@ struct MediaInfoView: View { if let imageUrl = episodeDetails["image"] as? String, !imageUrl.isEmpty { image = imageUrl } - if MetadataCacheManager.shared.isCachingEnabled { - let metadata = EpisodeMetadata( - title: title, - imageUrl: image, - anilistId: anilistId, - episodeNumber: episodeNumber - ) - - let cacheKey = "anilist_\(anilistId)_episode_\(episodeNumber)" - if let metadataData = metadata.toData() { - MetadataCacheManager.shared.storeMetadata( - metadataData, - forKey: cacheKey - ) - } - } let metadataInfo = EpisodeMetadataInfo( title: title, diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift index f9eb3e7..899b796 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift @@ -136,22 +136,14 @@ fileprivate struct SettingsButtonRow: View { } struct SettingsViewData: View { - @State private var showEraseAppDataAlert = false - @State private var showRemoveDocumentsAlert = false - @State private var showSizeAlert = false + @State private var showAlert = false @State private var cacheSizeText: String = "Calculating..." @State private var isCalculatingSize: Bool = false @State private var cacheSize: Int64 = 0 @State private var documentsSize: Int64 = 0 - @State private var movPkgSize: Int64 = 0 - @State private var showRemoveMovPkgAlert = false - @State private var isMetadataCachingEnabled: Bool = false - @State private var isImageCachingEnabled: Bool = true - @State private var isMemoryOnlyMode: Bool = false - @State private var showAlert = false enum ActiveAlert { - case eraseData, removeDocs, removeMovPkg + case eraseData, removeDocs } @State private var activeAlert: ActiveAlert = .eraseData @@ -160,48 +152,9 @@ struct SettingsViewData: View { return ScrollView { VStack(spacing: 24) { SettingsSection( - title: "Cache Settings", + title: "Cache", footer: "Caching helps reduce network usage and load content faster. You can disable it to save storage space." ) { - SettingsToggleRow( - icon: "doc.text", - title: "Enable Metadata Caching", - isOn: $isMetadataCachingEnabled - ) - .onChange(of: isMetadataCachingEnabled) { newValue in - MetadataCacheManager.shared.isCachingEnabled = newValue - if !newValue { - calculateCacheSize() - } - } - - SettingsToggleRow( - icon: "photo", - title: "Enable Image Caching", - isOn: $isImageCachingEnabled - ) - .onChange(of: isImageCachingEnabled) { newValue in - KingfisherCacheManager.shared.isCachingEnabled = newValue - if !newValue { - calculateCacheSize() - } - } - - if isMetadataCachingEnabled { - SettingsToggleRow( - icon: "memorychip", - title: "Memory-Only Mode", - isOn: $isMemoryOnlyMode - ) - .onChange(of: isMemoryOnlyMode) { newValue in - MetadataCacheManager.shared.isMemoryOnlyMode = newValue - if newValue { - MetadataCacheManager.shared.clearAllCache() - calculateCacheSize() - } - } - } - HStack { Image(systemName: "folder.badge.gearshape") .frame(width: 24, height: 24) @@ -250,16 +203,6 @@ struct SettingsViewData: View { ) Divider().padding(.horizontal, 16) - SettingsButtonRow( - icon: "arrow.down.circle", - title: "Remove Downloads", - subtitle: formatSize(movPkgSize), - action: { - showRemoveMovPkgAlert = true - } - ) - Divider().padding(.horizontal, 16) - SettingsButtonRow( icon: "exclamationmark.triangle", title: "Erase all App Data", @@ -274,9 +217,6 @@ struct SettingsViewData: View { .scrollViewBottomPadding() .navigationTitle("App Data") .onAppear { - isMetadataCachingEnabled = MetadataCacheManager.shared.isCachingEnabled - isImageCachingEnabled = KingfisherCacheManager.shared.isCachingEnabled - isMemoryOnlyMode = MetadataCacheManager.shared.isMemoryOnlyMode calculateCacheSize() updateSizes() } @@ -300,53 +240,44 @@ struct SettingsViewData: View { }, secondaryButton: .cancel() ) - case .removeMovPkg: - return Alert( - title: Text("Remove Downloads"), - message: Text("Are you sure you want to remove all Downloads?"), - primaryButton: .destructive(Text("Remove")) { - removeMovPkgFiles() - }, - secondaryButton: .cancel() - ) } } } - func calculateCacheSize() { isCalculatingSize = true cacheSizeText = "Calculating..." + DispatchQueue.global(qos: .background).async { - var totalSize: Int64 = 0 - let metadataSize = MetadataCacheManager.shared.getCacheSize() - totalSize += metadataSize - - KingfisherCacheManager.shared.calculateCacheSize { imageSize in - totalSize += Int64(imageSize) + if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { + let size = calculateDirectorySize(for: cacheURL) DispatchQueue.main.async { - self.cacheSizeText = KingfisherCacheManager.formatCacheSize(UInt(totalSize)) + self.cacheSize = size + self.cacheSizeText = formatSize(size) + self.isCalculatingSize = false + } + } else { + DispatchQueue.main.async { + self.cacheSizeText = "Unknown" self.isCalculatingSize = false } } } } - func clearAllCaches() { - MetadataCacheManager.shared.clearAllCache() - KingfisherCacheManager.shared.clearCache { - calculateCacheSize() + func updateSizes() { + DispatchQueue.global(qos: .background).async { + if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let size = calculateDirectorySize(for: documentsURL) + DispatchQueue.main.async { + self.documentsSize = size + } + } } - Logger.shared.log("All caches cleared", type: "General") } - func eraseAppData() { - if let domain = Bundle.main.bundleIdentifier { - UserDefaults.standard.removePersistentDomain(forName: domain) - UserDefaults.standard.synchronize() - Logger.shared.log("Cleared app data!", type: "General") - exit(0) - } + func clearAllCaches() { + clearCache() } func clearCache() { @@ -377,30 +308,24 @@ struct SettingsViewData: View { Logger.shared.log("All files in documents folder removed", type: "General") exit(0) } catch { - Logger.shared.log("Error removing files in documents folder: $error)", type: "Error") + Logger.shared.log("Error removing files in documents folder: \(error)", type: "Error") } } } - func removeMovPkgFiles() { - let fileManager = FileManager.default - if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - do { - let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) - for fileURL in fileURLs where fileURL.pathExtension == "movpkg" { - try fileManager.removeItem(at: fileURL) - } - Logger.shared.log("All Downloads files removed", type: "General") - updateSizes() - } catch { - Logger.shared.log("Error removing Downloads files: $error)", type: "Error") - } + func eraseAppData() { + if let domain = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: domain) + UserDefaults.standard.synchronize() + Logger.shared.log("Cleared app data!", type: "General") + exit(0) } } - func calculateDirectorySize(for url: URL) -> Int64 { + private func calculateDirectorySize(for url: URL) -> Int64 { let fileManager = FileManager.default var totalSize: Int64 = 0 + do { let contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.fileSizeKey]) for url in contents { @@ -412,41 +337,17 @@ struct SettingsViewData: View { } } } catch { - Logger.shared.log("Error calculating directory size: $error)", type: "Error") + Logger.shared.log("Error calculating directory size: \(error)", type: "Error") } + return totalSize } - func formatSize(_ bytes: Int64) -> String { + private func formatSize(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB] formatter.countStyle = .file return formatter.string(fromByteCount: bytes) } - - func updateSizes() { - if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { - cacheSize = calculateDirectorySize(for: cacheURL) - } - if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - documentsSize = calculateDirectorySize(for: documentsURL) - movPkgSize = calculateMovPkgSize(in: documentsURL) - } - } - - func calculateMovPkgSize(in url: URL) -> Int64 { - let fileManager = FileManager.default - var totalSize: Int64 = 0 - do { - let contents = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.fileSizeKey]) - for url in contents where url.pathExtension == "movpkg" { - let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey]) - totalSize += Int64(resourceValues.fileSize ?? 0) - } - } catch { - Logger.shared.log("Error calculating MovPkg size: $error)", type: "Error") - } - return totalSize - } } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 2118699..584f1ec 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -83,12 +83,8 @@ 1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */; }; 1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; - 1EA64DCD2DE5030100AC14BC /* ImageUpscaler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */; }; 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; 1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */; }; - 7205AEDB2DCCEF9500943F3F /* EpisodeMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */; }; - 7205AEDC2DCCEF9500943F3F /* MetadataCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205AED92DCCEF9500943F3F /* MetadataCacheManager.swift */; }; - 7205AEDD2DCCEF9500943F3F /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205AED82DCCEF9500943F3F /* KingfisherManager.swift */; }; 7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */; }; 722248602DCBAA2C00CABE2D /* M3U8StreamExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */; }; 722248632DCBAA4700CABE2D /* JSController-HeaderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722248612DCBAA4700CABE2D /* JSController-HeaderManager.swift */; }; @@ -98,9 +94,6 @@ 72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */; }; 727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */; }; 727220722DD642B100C2A4A2 /* JSController+MP4Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = 727220702DD642B100C2A4A2 /* JSController+MP4Download.swift */; }; - 72AC3A012DD4DAEB00C60B96 /* ImagePrefetchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AC39FE2DD4DAEA00C60B96 /* ImagePrefetchManager.swift */; }; - 72AC3A022DD4DAEB00C60B96 /* EpisodeMetadataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */; }; - 72AC3A032DD4DAEB00C60B96 /* PerformanceMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72AC39FF2DD4DAEA00C60B96 /* PerformanceMonitor.swift */; }; 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; /* End PBXBuildFile section */ @@ -181,12 +174,8 @@ 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = ""; }; 1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchPopupView.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; - 1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUpscaler.swift; sourceTree = ""; }; 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLib.swift; sourceTree = ""; }; - 7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeMetadata.swift; sourceTree = ""; }; - 7205AED82DCCEF9500943F3F /* KingfisherManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KingfisherManager.swift; sourceTree = ""; }; - 7205AED92DCCEF9500943F3F /* MetadataCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataCacheManager.swift; sourceTree = ""; }; 7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadModels.swift; sourceTree = ""; }; 7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = M3U8StreamExtractor.swift; sourceTree = ""; }; 722248612DCBAA4700CABE2D /* JSController-HeaderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-HeaderManager.swift"; sourceTree = ""; }; @@ -196,9 +185,6 @@ 72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDownloads.swift; sourceTree = ""; }; 7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-StreamTypeDownload.swift"; sourceTree = ""; }; 727220702DD642B100C2A4A2 /* JSController+MP4Download.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController+MP4Download.swift"; sourceTree = ""; }; - 72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeMetadataManager.swift; sourceTree = ""; }; - 72AC39FE2DD4DAEA00C60B96 /* ImagePrefetchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePrefetchManager.swift; sourceTree = ""; }; - 72AC39FF2DD4DAEA00C60B96 /* PerformanceMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMonitor.swift; sourceTree = ""; }; 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -327,7 +313,6 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( - 72AC3A002DD4DAEA00C60B96 /* Managers */, 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, 13DC0C412D2EC9BA00D0F966 /* Info.plist */, 13103E802D589D6C000F0673 /* Tracking Services */, @@ -395,7 +380,6 @@ 04F08EE02DE10C22006B29D9 /* Models */, 04F08EDD2DE10C05006B29D9 /* TabBar */, 04F08EDA2DE10BE3006B29D9 /* ProgressiveBlurView */, - 7205AEDA2DCCEF9500943F3F /* Cache */, 13D842532D45266900EBBFA6 /* Drops */, 1399FAD12D3AB33D00E97C31 /* Logger */, 133D7C882D2BE2640075467E /* Modules */, @@ -606,16 +590,6 @@ path = Components; sourceTree = ""; }; - 7205AEDA2DCCEF9500943F3F /* Cache */ = { - isa = PBXGroup; - children = ( - 7205AED72DCCEF9500943F3F /* EpisodeMetadata.swift */, - 7205AED82DCCEF9500943F3F /* KingfisherManager.swift */, - 7205AED92DCCEF9500943F3F /* MetadataCacheManager.swift */, - ); - path = Cache; - sourceTree = ""; - }; 72443C832DC8046500A61321 /* DownloadUtils */ = { isa = PBXGroup; children = ( @@ -626,17 +600,6 @@ path = DownloadUtils; sourceTree = ""; }; - 72AC3A002DD4DAEA00C60B96 /* Managers */ = { - isa = PBXGroup; - children = ( - 1EA64DCC2DE5030100AC14BC /* ImageUpscaler.swift */, - 72AC39FD2DD4DAEA00C60B96 /* EpisodeMetadataManager.swift */, - 72AC39FE2DD4DAEA00C60B96 /* ImagePrefetchManager.swift */, - 72AC39FF2DD4DAEA00C60B96 /* PerformanceMonitor.swift */, - ); - path = Managers; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -728,10 +691,7 @@ 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */, 1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, - 7205AEDB2DCCEF9500943F3F /* EpisodeMetadata.swift in Sources */, 04F08EDC2DE10BF3006B29D9 /* ProgressiveBlurView.swift in Sources */, - 7205AEDC2DCCEF9500943F3F /* MetadataCacheManager.swift in Sources */, - 7205AEDD2DCCEF9500943F3F /* KingfisherManager.swift in Sources */, 1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */, 136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */, 13DB46902D900A38008CBC03 /* URL.swift in Sources */, @@ -788,15 +748,11 @@ 727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */, 727220722DD642B100C2A4A2 /* JSController+MP4Download.swift in Sources */, 132FC5B32DE31DAE009A80F7 /* SettingsViewAlternateAppIconPicker.swift in Sources */, - 1EA64DCD2DE5030100AC14BC /* ImageUpscaler.swift in Sources */, 0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */, 0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */, 0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */, 04F08EDF2DE10C1D006B29D9 /* TabBar.swift in Sources */, - 72AC3A012DD4DAEB00C60B96 /* ImagePrefetchManager.swift in Sources */, - 72AC3A022DD4DAEB00C60B96 /* EpisodeMetadataManager.swift in Sources */, - 72AC3A032DD4DAEB00C60B96 /* PerformanceMonitor.swift in Sources */, 72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */, 13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */, 7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */,