import Foundation import UIKit /// Album art: a memory cache over a big disk URLCache, so downloaded albums keep their covers /// offline. Stands in for Coil on Android. final class ArtLoader: @unchecked Sendable { static let shared = ArtLoader() private let memory = NSCache() private let session: URLSession private let lock = NSLock() private var inflight: [URL: Task] = [:] init() { let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("art", isDirectory: true) let cfg = URLSessionConfiguration.default cfg.urlCache = URLCache(memoryCapacity: 16 << 20, diskCapacity: 512 << 20, directory: dir) cfg.requestCachePolicy = .returnCacheDataElseLoad cfg.timeoutIntervalForRequest = 20 session = URLSession(configuration: cfg) memory.totalCostLimit = 64 << 20 } func cached(_ url: URL) -> UIImage? { memory.object(forKey: url as NSURL) } func image(_ url: URL) async -> UIImage? { if let img = cached(url) { return img } if let t = lock.withLock({ inflight[url] }) { return await t.value } let t = Task { [session] in guard let (data, resp) = try? await session.data(from: url), (resp as? HTTPURLResponse).map({ (200..<300).contains($0.statusCode) }) ?? true, let img = UIImage(data: data) else { return nil } let decoded = await img.byPreparingForDisplay() ?? img return decoded } lock.withLock { inflight[url] = t } let img = await t.value lock.withLock { inflight[url] = nil } if let img { memory.setObject(img, forKey: url as NSURL, cost: Int(img.size.width * img.size.height * img.scale * img.scale * 4)) } return img } func prefetch(_ url: URL) { Task.detached(priority: .utility) { _ = await self.image(url) } } }