luminate-old/Sources/LuminateCore/ImageService.swift

110 lines
3.9 KiB
Swift

//
// ImageService.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Foundation
import Logging
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public actor ImageService {
private let cacheDir: URL
private let memoryCache = NSCache<NSString, NSData>()
private var activeDownloads = 0
private var imagesDownloaded = 0
private let maxConcurrent: Int
private var pendingContinuations: [CheckedContinuation<Void, Never>] = []
private var logger: Logger = .init(label: "dev.bscubed.Luminate")
public init(cacheDir: URL? = nil, maxConcurrent: Int = 6) {
let defaultCache = FileManager.default.urls(
for: .cachesDirectory, in: .userDomainMask
).first!.appendingPathComponent("luminate/images")
self.cacheDir = cacheDir ?? defaultCache
self.maxConcurrent = maxConcurrent
try? FileManager.default.createDirectory(
at: self.cacheDir, withIntermediateDirectories: true)
logger.debug("New ImageService created")
}
public func loadImage(url: URL) async throws -> Data {
let key = url.absoluteString as NSString
if let cached = memoryCache.object(forKey: key) {
logger.debug("Hit memory cache for \(key)")
return cached as Data
}
let diskURL = diskCacheURL(for: url)
if let data = try? Data(contentsOf: diskURL) {
memoryCache.setObject(data as NSData, forKey: key)
logger.debug("Hit disk cache for \(key)")
return data
}
if activeDownloads >= maxConcurrent {
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
logger.debug("Idk what this is doing tbh")
pendingContinuations.append(continuation)
}
}
activeDownloads += 1
defer {
activeDownloads -= 1
if !pendingContinuations.isEmpty {
let next = pendingContinuations.removeFirst()
next.resume()
}
}
logger.debug("Started downloading \(url)")
let (data, _) = try await URLSession.shared.data(from: url)
let nsData = data as NSData
memoryCache.setObject(nsData, forKey: key)
try? data.write(to: diskURL)
logger.debug("Finished downloading \(url)")
return data
}
public func prefetch(urls: [URL]) async {
logger.debug("Prefetching \(urls)")
await withTaskGroup(of: Void.self) { group in
for url in urls {
group.addTask { _ = try? await self.loadImage(url: url) }
}
}
}
public func clearCache() {
logger.debug("Clearing all cache...")
memoryCache.removeAllObjects()
try? FileManager.default.removeItem(at: cacheDir)
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
}
private func diskCacheURL(for url: URL) -> URL {
let diskKey = url.absoluteString.data(using: .utf8)!.base64EncodedString()
.replacingOccurrences(of: "/", with: "_")
return cacheDir.appendingPathComponent(diskKey)
}
}