| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- 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<String> = [
- "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<UIImage>.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<UIImage>.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
- }
- }
|