import Foundation /// Resolves group template strings using track metadata. /// Supported placeholders: {Artist}, {Album}, {Genre}, {Year}, {Folder}, {Format}, {BPM}, {Key} struct GroupTemplateResolver { /// Preset templates for quick selection. static let presets: [(name: String, template: String)] = [ ("No Grouping", ""), ("Album (Date)", "{Album} ({Date})"), ("Artist — Album", "{Artist} — {Album}"), ("Artist", "{Artist}"), ("Album", "{Album}"), ("Genre", "{Genre}"), ("Folder", "{Folder}"), ("Format", "{Format}"), ("BPM Range", "{BPM}"), ] /// Available placeholders with descriptions. static let placeholders: [(token: String, description: String)] = [ ("{Artist}", "Track artist"), ("{Album}", "Album name"), ("{Genre}", "Genre"), ("{Date}", "Release date/year from metadata"), ("{Folder}", "Parent folder name"), ("{Format}", "File format (FLAC, MP3, etc.)"), ("{BPM}", "BPM (rounded to nearest 10)"), ("{Key}", "Musical key"), ] /// Resolve a template string for a track, returning the group header text. static func resolve(template: String, for track: Track) -> String { guard !template.isEmpty else { return "" } var result = template result = result.replacingOccurrences(of: "{Artist}", with: track.artist.isEmpty ? "Unknown Artist" : track.artist) result = result.replacingOccurrences(of: "{Album}", with: track.album.isEmpty ? "Unknown Album" : track.album) result = result.replacingOccurrences(of: "{Genre}", with: track.genre.isEmpty ? "Unknown Genre" : track.genre) let yearStr = track.year.map { String($0) } ?? "" result = result.replacingOccurrences(of: "{Year}", with: yearStr) result = result.replacingOccurrences(of: "{Date}", with: yearStr) result = result.replacingOccurrences(of: "{Folder}", with: folderName(for: track)) result = result.replacingOccurrences(of: "{Format}", with: track.fileFormat.isEmpty ? "Unknown" : track.fileFormat) result = result.replacingOccurrences(of: "{BPM}", with: bpmRange(for: track)) result = result.replacingOccurrences(of: "{Key}", with: track.musicalKey ?? "Unknown Key") // Clean up empty brackets etc. result = result.replacingOccurrences(of: "()", with: "") result = result.replacingOccurrences(of: "[]", with: "") result = result.replacingOccurrences(of: " — Unknown Artist", with: "") result = result.trimmingCharacters(in: .whitespaces) return result.isEmpty ? "Ungrouped" : result } private static func folderName(for track: Track) -> String { let components = track.filePath.split(separator: "/") if components.count >= 2 { return String(components[components.count - 2]) } return "Root" } private static func bpmRange(for track: Track) -> String { guard let bpm = track.bpm else { return "No BPM" } let rounded = Int(bpm / 10) * 10 return "\(rounded)-\(rounded + 10) BPM" } }