velody/packages/apple/VelodyPersistence/Sources/VelodyPersistence/LocalArtworkStore.swift
2026-05-31 01:20:56 +02:00

159 lines
4.7 KiB
Swift

import Foundation
public enum LocalArtworkStoreError: LocalizedError, Equatable, Sendable {
case emptyArtworkData
case missingLocalFile(path: String)
public var errorDescription: String? {
switch self {
case .emptyArtworkData:
return "The local artwork data was empty."
case let .missingLocalFile(path):
return "The local artwork file is missing: \(path)"
}
}
}
public protocol LocalArtworkStore: Actor {
func saveArtwork(
_ data: Data,
sha256: String,
mimeType: String
) async throws -> String
func readArtwork(at localFilePath: String) async throws -> Data
func fileExists(at localFilePath: String) async -> Bool
}
public actor FileLocalArtworkStore: LocalArtworkStore {
private let baseDirectoryURL: URL
private let fileManager: FileManager
public init(
baseDirectoryURL: URL? = nil,
fileManager: FileManager = .default
) throws {
self.fileManager = fileManager
if let baseDirectoryURL {
self.baseDirectoryURL = baseDirectoryURL
} else {
self.baseDirectoryURL = try Self.defaultBaseDirectoryURL(fileManager: fileManager)
}
}
public func saveArtwork(
_ data: Data,
sha256: String,
mimeType: String
) async throws -> String {
guard !data.isEmpty else {
throw LocalArtworkStoreError.emptyArtworkData
}
try fileManager.createDirectory(
at: baseDirectoryURL,
withIntermediateDirectories: true
)
let fileURL = localFileURL(sha256: sha256, mimeType: mimeType)
if fileManager.fileExists(atPath: fileURL.path) {
return fileURL.standardizedFileURL.path
}
try data.write(to: fileURL, options: .atomic)
return fileURL.standardizedFileURL.path
}
public func readArtwork(at localFilePath: String) async throws -> Data {
let fileURL = URL(fileURLWithPath: localFilePath).standardizedFileURL
guard fileManager.fileExists(atPath: fileURL.path) else {
throw LocalArtworkStoreError.missingLocalFile(path: localFilePath)
}
return try Data(contentsOf: fileURL)
}
public func fileExists(at localFilePath: String) async -> Bool {
let fileURL = URL(fileURLWithPath: localFilePath).standardizedFileURL
return fileManager.fileExists(atPath: fileURL.path)
}
private static func defaultBaseDirectoryURL(fileManager: FileManager) throws -> URL {
guard let applicationSupportURL = fileManager.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first else {
throw CocoaError(.fileNoSuchFile)
}
return applicationSupportURL
.appendingPathComponent("Velody", isDirectory: true)
.appendingPathComponent("local-artwork", isDirectory: true)
}
private func localFileURL(sha256: String, mimeType: String) -> URL {
baseDirectoryURL.appendingPathComponent(
"\(sha256).\(Self.fileExtension(for: mimeType))"
)
}
private static func fileExtension(for mimeType: String) -> String {
switch mimeType.lowercased() {
case "image/jpeg", "image/jpg":
return "jpg"
case "image/png":
return "png"
default:
return "img"
}
}
}
public actor InMemoryLocalArtworkStore: LocalArtworkStore {
private var files: [String: Data]
public init(files: [String: Data] = [:]) {
self.files = files
}
public func saveArtwork(
_ data: Data,
sha256: String,
mimeType: String
) async throws -> String {
guard !data.isEmpty else {
throw LocalArtworkStoreError.emptyArtworkData
}
let localFilePath = Self.localFilePath(sha256: sha256, mimeType: mimeType)
files[localFilePath] = data
return localFilePath
}
public func readArtwork(at localFilePath: String) async throws -> Data {
guard let data = files[localFilePath] else {
throw LocalArtworkStoreError.missingLocalFile(path: localFilePath)
}
return data
}
public func fileExists(at localFilePath: String) async -> Bool {
files[localFilePath] != nil
}
private static func localFilePath(sha256: String, mimeType: String) -> String {
let fileExtension: String
switch mimeType.lowercased() {
case "image/jpeg", "image/jpg":
fileExtension = "jpg"
case "image/png":
fileExtension = "png"
default:
fileExtension = "img"
}
return "/in-memory/local-artwork/\(sha256).\(fileExtension)"
}
}