import AVFoundation import Foundation import UIKit /// Loads album artwork from folder images or embedded metadata. actor ArtworkService { static let shared = ArtworkService() /// Cache keyed by track file path (not folder) so each track gets its own artwork. private var cache: [String: UIImage?] = [:] private static let coverFileNames: [String] = [ "cover", "folder", "front", "album", "albumart", "albumartsmall", "thumb", "artwork", "art", "Cover", "Folder", "Front", "Album" ] private static let imageExtensions: Set = [ "jpg", "jpeg", "png", "bmp", "gif", "tiff", "webp" ] // MARK: - Public API /// Get artwork for a track. Pass the URL directly to avoid SwiftData model access issues. func artwork(for trackURL: URL) async -> UIImage? { let trackPath = trackURL.path // Check cache (nil value means we already tried and found nothing) if let cached = cache[trackPath] { return cached } // Only access files within the app sandbox let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! guard trackPath.hasPrefix(docsDir.path) else { cache[trackPath] = Optional.none return nil } // 1. Try embedded metadata FIRST (most accurate per-track) if FileManager.default.fileExists(atPath: trackPath), let embedded = await extractEmbeddedArtwork(from: trackURL) { cache[trackPath] = embedded return embedded } // 2. Try folder images (cover.jpg etc.) let folderURL = trackURL.deletingLastPathComponent() if let folderArt = findFolderArtwork(in: folderURL) { cache[trackPath] = folderArt return folderArt } // Cache nil so we don't retry cache[trackPath] = Optional.none return nil } func clearCache() { cache.removeAll() } // MARK: - Folder Artwork private func findFolderArtwork(in folderURL: URL) -> UIImage? { let fm = FileManager.default for name in Self.coverFileNames { for ext in Self.imageExtensions { let candidate = folderURL.appendingPathComponent("\(name).\(ext)") if fm.fileExists(atPath: candidate.path), let data = try? Data(contentsOf: candidate), let image = UIImage(data: data) { return image } } } // Fallback: any image file in the folder if let contents = try? fm.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) { for fileURL in contents { if Self.imageExtensions.contains(fileURL.pathExtension.lowercased()), let data = try? Data(contentsOf: fileURL), let image = UIImage(data: data) { return image } } } return nil } // MARK: - Embedded Artwork private func extractEmbeddedArtwork(from url: URL) async -> UIImage? { let asset = AVURLAsset(url: url) guard let metadata = try? await asset.load(.metadata) else { return nil } for item in metadata { guard item.commonKey == .commonKeyArtwork else { continue } if let data = try? await item.load(.dataValue), let image = UIImage(data: data) { return image } } return nil } }