import Foundation /// Generates filenames from audio metadata using configurable templates. /// /// Template variables (enclosed in `{}`): /// {artist} - Track artist /// {album} - Album name /// {title} - Track title /// {genre} - Genre /// {bpm} - BPM (rounded) /// {key} - Musical key /// {year} - Release year from metadata /// {track} - Track number in playlist (zero-padded) /// {duration} - Duration as M:SS /// {format} - File format extension /// {samplerate} - Sample rate /// {bitdepth} - Bit depth /// /// Example: "{artist} - {album} - {title}" → "Raekwon - Only Built 4 Cuban Linx - Spot Rusherz" struct FileNameTemplate { /// Default template static let defaultTemplate = "{artist} - {album} - {title}" /// All available template variables with descriptions. static let availableVariables: [(token: String, description: String)] = [ ("{track}", "Track number in playlist (zero-padded)"), ("{artist}", "Artist name"), ("{album}", "Album name"), ("{title}", "Track title"), ("{genre}", "Genre"), ("{bpm}", "BPM (rounded)"), ("{key}", "Musical key"), ("{year}", "Release year"), ("{duration}", "Duration (M:SS)"), ("{format}", "File format (MP3, WAV, etc.)"), ("{samplerate}", "Sample rate (e.g. 44100)"), ("{bitdepth}", "Bit depth (e.g. 16)"), ] /// Some common presets. static let presets: [(name: String, template: String)] = [ ("Artist - Album - Title", "{artist} - {album} - {title}"), ("## Artist - Title", "{track} {artist} - {title}"), ("## Title", "{track} {title}"), ("Artist - Title [BPM Key]", "{artist} - {title} [{bpm} {key}]"), ("Album - ## Title", "{album} - {track} {title}"), ("Artist - Title (Format)", "{artist} - {title} ({format})"), ] /// Generate a filename from a template string and track metadata. static func generate( template: String, track: Track, playlistIndex: Int, totalTracks: Int ) -> String { let padWidth = totalTracks >= 100 ? 3 : 2 var result = template result = result.replacingOccurrences(of: "{track}", with: String(format: "%0\(padWidth)d", playlistIndex + 1)) 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: "{title}", with: track.title) result = result.replacingOccurrences(of: "{genre}", with: track.genre.isEmpty ? "Unknown" : track.genre) result = result.replacingOccurrences(of: "{bpm}", with: track.bpm.map { String(format: "%.0f", $0) } ?? "") result = result.replacingOccurrences(of: "{key}", with: track.musicalKey ?? "") result = result.replacingOccurrences(of: "{year}", with: track.year.map { String($0) } ?? "") result = result.replacingOccurrences(of: "{duration}", with: track.formattedDuration) result = result.replacingOccurrences(of: "{format}", with: track.fileFormat) result = result.replacingOccurrences(of: "{samplerate}", with: "\(Int(track.sampleRate))") result = result.replacingOccurrences(of: "{bitdepth}", with: "\(track.bitDepth)") // Clean up: remove double separators from empty fields, trim result = result.replacingOccurrences(of: " ", with: " ") result = result.replacingOccurrences(of: " - - ", with: " - ") result = result.replacingOccurrences(of: "- -", with: "-") result = result.replacingOccurrences(of: "[]", with: "") result = result.replacingOccurrences(of: "()", with: "") result = result.trimmingCharacters(in: .whitespaces) result = result.trimmingCharacters(in: CharacterSet(charactersIn: "- ")) // Sanitize for filesystem return sanitizeFilename(result) } /// Preview what a filename would look like with a given template. static func preview(template: String) -> String { let fakeTrack = Track( title: "Spot Rusherz", artist: "Raekwon", album: "Only Built 4 Cuban Linx", genre: "Hip-Hop", filePath: "/fake.mp3", duration: 193, fileFormat: "MP3" ) fakeTrack.bpm = 95 fakeTrack.musicalKey = "Dm" fakeTrack.year = 1995 return generate(template: template, track: fakeTrack, playlistIndex: 0, totalTracks: 18) } // MARK: - Helpers private static func sanitizeFilename(_ name: String) -> String { let illegal = CharacterSet(charactersIn: "/\\:*?\"<>|") return name.components(separatedBy: illegal).joined(separator: "_") } }