| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- 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: "_")
- }
- }
|