GroupTemplateResolver.swift 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. import Foundation
  2. /// Resolves group template strings using track metadata.
  3. /// Supported placeholders: {Artist}, {Album}, {Genre}, {Date}, {Folder}, {Format}, {BPM}, {Key}
  4. struct GroupTemplateResolver {
  5. /// Preset templates for quick selection.
  6. static let presets: [(name: String, template: String)] = [
  7. ("No Grouping", ""),
  8. ("Album (Date)", "{Album} ({Date})"),
  9. ("Artist — Album", "{Artist} — {Album}"),
  10. ("Artist", "{Artist}"),
  11. ("Album", "{Album}"),
  12. ("Genre", "{Genre}"),
  13. ("Folder", "{Folder}"),
  14. ("Format", "{Format}"),
  15. ("BPM Range", "{BPM}"),
  16. ]
  17. /// Available placeholders with descriptions.
  18. static let placeholders: [(token: String, description: String)] = [
  19. ("{Artist}", "Track artist"),
  20. ("{Album}", "Album name"),
  21. ("{Genre}", "Genre"),
  22. ("{Date}", "Release date/year from metadata"),
  23. ("{Folder}", "Parent folder name"),
  24. ("{Format}", "File format (FLAC, MP3, etc.)"),
  25. ("{BPM}", "BPM (rounded to nearest 10)"),
  26. ("{Key}", "Musical key"),
  27. ]
  28. /// Resolve a template string for a track, returning the group header text.
  29. static func resolve(template: String, for track: Track) -> String {
  30. guard !template.isEmpty else { return "" }
  31. var result = template
  32. result = result.replacingOccurrences(of: "{Artist}", with: track.artist.isEmpty ? "Unknown Artist" : track.artist)
  33. result = result.replacingOccurrences(of: "{Album}", with: track.album.isEmpty ? "Unknown Album" : track.album)
  34. result = result.replacingOccurrences(of: "{Genre}", with: track.genre.isEmpty ? "Unknown Genre" : track.genre)
  35. let yearStr = track.year.map { String($0) } ?? ""
  36. result = result.replacingOccurrences(of: "{Year}", with: yearStr)
  37. result = result.replacingOccurrences(of: "{Date}", with: yearStr)
  38. result = result.replacingOccurrences(of: "{Folder}", with: folderName(for: track))
  39. result = result.replacingOccurrences(of: "{Format}", with: track.fileFormat.isEmpty ? "Unknown" : track.fileFormat)
  40. result = result.replacingOccurrences(of: "{BPM}", with: bpmRange(for: track))
  41. result = result.replacingOccurrences(of: "{Key}", with: track.musicalKey ?? "Unknown Key")
  42. // Clean up empty brackets etc.
  43. result = result.replacingOccurrences(of: "()", with: "")
  44. result = result.replacingOccurrences(of: "[]", with: "")
  45. result = result.trimmingCharacters(in: .whitespaces)
  46. return result.isEmpty ? "Ungrouped" : result
  47. }
  48. private static func folderName(for track: Track) -> String {
  49. let components = track.filePath.split(separator: "/")
  50. if components.count >= 2 {
  51. return String(components[components.count - 2])
  52. }
  53. return "Root"
  54. }
  55. private static func bpmRange(for track: Track) -> String {
  56. guard let bpm = track.bpm else { return "No BPM" }
  57. let rounded = Int(bpm / 10) * 10
  58. return "\(rounded)-\(rounded + 10) BPM"
  59. }
  60. }