5050// osLink is a package-level alias for os.Link to allow tests to inject failures.
5151var osLink = os .Link
5252
53+ // rootCmd is the top-level Cobra command that defines the CLI interface.
5354var rootCmd = & cobra.Command {
5455 Use : "lilt <source_directory>" ,
5556 Short : "Convert Hi-Res FLAC/ALAC files to 16-bit FLAC files" ,
@@ -74,6 +75,7 @@ Licensed under MIT License`,
7475 Version : version ,
7576}
7677
78+ // init registers all command-line flags and sets default configuration values.
7779func init () {
7880 rootCmd .Flags ().StringVar (& config .TargetDir , "target-dir" , "./transcoded" , "Specify target directory" )
7981 rootCmd .Flags ().BoolVar (& config .CopyImages , "copy-images" , false , "Copy JPG and PNG files" )
@@ -88,13 +90,17 @@ func init() {
8890 config .SoxCommand = "sox"
8991}
9092
93+ // main is the program entry point. It executes the root Cobra command and
94+ // exits with a non-zero code on error.
9195func main () {
9296 if err := rootCmd .Execute (); err != nil {
9397 fmt .Fprintf (os .Stderr , "Error: %v\n " , err )
9498 os .Exit (1 )
9599 }
96100}
97101
102+ // runConverter is the main command handler. It validates arguments, sets up
103+ // the SoX command, processes audio files, and optionally copies image files.
98104func runConverter (cmd * cobra.Command , args []string ) error {
99105 if selfUpdateFlag {
100106 if len (args ) > 0 {
@@ -148,6 +154,9 @@ func runConverter(cmd *cobra.Command, args []string) error {
148154 return nil
149155}
150156
157+ // setupSoxCommand validates the availability of SoX (or Docker) and FFmpeg
158+ // based on the current configuration. It resolves absolute paths when using
159+ // Docker mode and checks for ALAC files when metadata preservation is disabled.
151160func setupSoxCommand () error {
152161 if config .UseDocker {
153162 // Check if docker is installed
@@ -192,6 +201,8 @@ func setupSoxCommand() error {
192201 return nil
193202}
194203
204+ // hasALACFiles walks the given directory and returns true if any .m4a file
205+ // (ALAC container) is found, stopping the walk early.
195206func hasALACFiles (dir string ) (bool , error ) {
196207 hasALAC := false
197208 err := filepath .Walk (dir , func (path string , info os.FileInfo , err error ) error {
@@ -208,6 +219,9 @@ func hasALACFiles(dir string) (bool, error) {
208219 return hasALAC , err
209220}
210221
222+ // processAudioFiles walks the source directory and processes each audio file
223+ // (FLAC, MP3, or ALAC) according to the current configuration, including
224+ // format enforcement, conversion, and metadata preservation.
211225func processAudioFiles () error {
212226 return filepath .Walk (config .SourceDir , func (path string , info os.FileInfo , err error ) error {
213227 if err != nil {
@@ -298,6 +312,9 @@ func processAudioFiles() error {
298312 })
299313}
300314
315+ // processAudioFileWithEnforcedFormat handles a single audio file when the
316+ // --enforce-output-format flag is set, routing to the appropriate target
317+ // format handler (FLAC, MP3, or ALAC).
301318func processAudioFileWithEnforcedFormat (sourcePath , targetPath , sourceExt string ) error {
302319 // Get audio info for source file
303320 var audioInfo * AudioInfo
@@ -332,6 +349,9 @@ func processAudioFileWithEnforcedFormat(sourcePath, targetPath, sourceExt string
332349 }
333350}
334351
352+ // processToFLAC converts the source file to FLAC format according to the
353+ // --enforce-output-format=flac flag. MP3 sources are copied as-is since
354+ // transcoding lossy to lossless is not useful.
335355func processToFLAC (sourcePath , targetPath , sourceExt string , audioInfo * AudioInfo ) error {
336356 // Change target extension to .flac
337357 targetPath = changeExtensionToFlac (targetPath )
@@ -370,6 +390,8 @@ func processToFLAC(sourcePath, targetPath, sourceExt string, audioInfo *AudioInf
370390 return fmt .Errorf ("unsupported source format for FLAC conversion: %s" , sourceExt )
371391}
372392
393+ // processToMP3 converts the source file to 320kbps MP3 according to the
394+ // --enforce-output-format=mp3 flag. Already-MP3 files are copied as-is.
373395func processToMP3 (sourcePath , targetPath , sourceExt string , audioInfo * AudioInfo ) error {
374396 // Change target extension to .mp3
375397 targetPath = changeExtensionToMP3 (targetPath )
@@ -384,6 +406,9 @@ func processToMP3(sourcePath, targetPath, sourceExt string, audioInfo *AudioInfo
384406 return convertToMP3 (sourcePath , targetPath , audioInfo )
385407}
386408
409+ // processToALAC converts the source file to ALAC (M4A) according to the
410+ // --enforce-output-format=alac flag. Already-ALAC files at 16-bit are
411+ // copied as-is; MP3 sources are copied without conversion.
387412func processToALAC (sourcePath , targetPath , sourceExt string , audioInfo * AudioInfo ) error {
388413 // Change target extension to .m4a
389414 targetPath = changeExtensionToM4A (targetPath )
@@ -416,6 +441,9 @@ func processToALAC(sourcePath, targetPath, sourceExt string, audioInfo *AudioInf
416441 return fmt .Errorf ("unsupported source format for ALAC conversion: %s" , sourceExt )
417442}
418443
444+ // getAudioInfo returns audio information (bit depth, sample rate, format)
445+ // for the given file by dispatching to the appropriate format-specific
446+ // inspector (FLAC via SoX, ALAC via ffprobe).
419447func getAudioInfo (filePath string ) (* AudioInfo , error ) {
420448 ext := strings .ToLower (filepath .Ext (filePath ))
421449
@@ -426,6 +454,8 @@ func getAudioInfo(filePath string) (*AudioInfo, error) {
426454 }
427455}
428456
457+ // getFLACInfo runs SoX's --i flag on the given FLAC file and parses the
458+ // output to determine the bit depth and sample rate.
429459func getFLACInfo (filePath string ) (* AudioInfo , error ) {
430460 var cmd * exec.Cmd
431461
@@ -454,6 +484,8 @@ func getFLACInfo(filePath string) (*AudioInfo, error) {
454484 return audioInfo , nil
455485}
456486
487+ // getALACInfo uses ffprobe to extract the sample rate and bit depth from
488+ // an ALAC (M4A) file, supporting both local and Docker execution modes.
457489func getALACInfo (filePath string ) (* AudioInfo , error ) {
458490 var cmd * exec.Cmd
459491
@@ -481,6 +513,9 @@ func getALACInfo(filePath string) (*AudioInfo, error) {
481513 return parseALACInfo (string (output ))
482514}
483515
516+ // parseALACInfo parses ffprobe CSV output (sample_rate,bits_per_raw_sample)
517+ // and returns the audio information. It skips lines with invalid or out-of-range
518+ // values and only returns the first valid audio stream found.
484519func parseALACInfo (info string ) (* AudioInfo , error ) {
485520 lines := strings .Split (strings .TrimSpace (info ), "\n " )
486521 if len (lines ) == 0 {
@@ -524,21 +559,27 @@ func parseALACInfo(info string) (*AudioInfo, error) {
524559 return nil , fmt .Errorf ("no valid audio stream information found" )
525560}
526561
562+ // changeExtensionToFlac replaces the file extension with .flac.
527563func changeExtensionToFlac (filePath string ) string {
528564 ext := filepath .Ext (filePath )
529565 return strings .TrimSuffix (filePath , ext ) + ".flac"
530566}
531567
568+ // changeExtensionToMP3 replaces the file extension with .mp3.
532569func changeExtensionToMP3 (filePath string ) string {
533570 ext := filepath .Ext (filePath )
534571 return strings .TrimSuffix (filePath , ext ) + ".mp3"
535572}
536573
574+ // changeExtensionToM4A replaces the file extension with .m4a.
537575func changeExtensionToM4A (filePath string ) string {
538576 ext := filepath .Ext (filePath )
539577 return strings .TrimSuffix (filePath , ext ) + ".m4a"
540578}
541579
580+ // convertToMP3 transcodes the source audio file to 320kbps MP3 using SoX,
581+ // then optionally preserves metadata via FFmpeg. The target sample rate is
582+ // chosen based on the source rate family (48kHz or 44.1kHz).
542583func convertToMP3 (sourcePath , targetPath string , audioInfo * AudioInfo ) error {
543584 // MP3 conversion: Use SoX to convert audio, then FFmpeg to preserve metadata
544585 var tempPath string
@@ -598,6 +639,9 @@ func convertToMP3(sourcePath, targetPath string, audioInfo *AudioInfo) error {
598639 return nil
599640}
600641
642+ // convertToALAC transcodes the source audio file to ALAC (M4A) via a two-step
643+ // process: SoX downsamples to an intermediate FLAC (if needed), then FFmpeg
644+ // encodes to ALAC while preserving metadata from the original file.
601645func convertToALAC (sourcePath , targetPath string , audioInfo * AudioInfo ) error {
602646 // ALAC conversion:
603647 // To preserve the best quality and metadata:
@@ -735,6 +779,8 @@ func convertToALAC(sourcePath, targetPath string, audioInfo *AudioInfo) error {
735779 return nil
736780}
737781
782+ // processAudioFile dispatches to the correct processing function based on the
783+ // audio format (FLAC or ALAC).
738784func processAudioFile (sourcePath , targetPath string , audioInfo * AudioInfo , needsConversion bool , bitrateArgs , sampleRateArgs []string ) error {
739785 if audioInfo .Format == "alac" {
740786 return processALAC (sourcePath , targetPath , needsConversion , bitrateArgs , sampleRateArgs )
@@ -743,6 +789,9 @@ func processAudioFile(sourcePath, targetPath string, audioInfo *AudioInfo, needs
743789 }
744790}
745791
792+ // processALAC converts an ALAC file to FLAC. When conversion is needed, it
793+ // uses a two-step process (FFmpeg for format conversion, then SoX for quality
794+ // adjustment). Metadata is preserved via FFmpeg when enabled.
746795func processALAC (sourcePath , targetPath string , needsConversion bool , bitrateArgs , sampleRateArgs []string ) error {
747796 var tempPath string
748797
@@ -863,6 +912,9 @@ func processALAC(sourcePath, targetPath string, needsConversion bool, bitrateArg
863912 return nil
864913}
865914
915+ // parseAudioInfo parses SoX's --i output to extract the bit depth and sample
916+ // rate from a FLAC file. Returns an AudioInfo with zero values if parsing
917+ // fails silently.
866918func parseAudioInfo (info string ) (* AudioInfo , error ) {
867919 audioInfo := & AudioInfo {}
868920 scanner := bufio .NewScanner (strings .NewReader (info ))
@@ -889,6 +941,9 @@ func parseAudioInfo(info string) (*AudioInfo, error) {
889941 return audioInfo , nil
890942}
891943
944+ // determineConversion checks whether the audio file needs bit depth or sample
945+ // rate conversion. It returns the required SoX arguments for both. Files at
946+ // 16-bit/44.1kHz or 16-bit/48kHz do not need conversion.
892947func determineConversion (info * AudioInfo ) (bool , []string , []string ) {
893948 needsConversion := false
894949 var bitrateArgs []string
@@ -913,6 +968,9 @@ func determineConversion(info *AudioInfo) (bool, []string, []string) {
913968 return needsConversion , bitrateArgs , sampleRateArgs
914969}
915970
971+ // processFlac transcodes a FLAC file to reduced bit depth / sample rate
972+ // using SoX, then optionally merges metadata from the source via FFmpeg.
973+ // If no conversion is needed, it copies the file directly.
916974func processFlac (sourcePath , targetPath string , needsConversion bool , bitrateArgs , sampleRateArgs []string ) error {
917975 if ! needsConversion {
918976 return copyFile (sourcePath , targetPath )
@@ -981,16 +1039,23 @@ func processFlac(sourcePath, targetPath string, needsConversion bool, bitrateArg
9811039 return nil
9821040}
9831041
1042+ // getDockerPath converts a host-side file path into a container-side source
1043+ // path by computing the relative path from the source directory.
9841044func getDockerPath (hostPath string ) string {
9851045 relPath := normalizeForDocker (config .SourceDir , hostPath )
9861046 return "/source/" + relPath
9871047}
9881048
1049+ // getDockerTargetPath converts a host-side file path into a container-side
1050+ // target path by computing the relative path from the target directory.
9891051func getDockerTargetPath (hostPath string ) string {
9901052 relPath := normalizeForDocker (config .TargetDir , hostPath )
9911053 return "/target/" + relPath
9921054}
9931055
1056+ // normalizeForDocker computes a clean relative path suitable for use inside
1057+ // a Docker volume mount. It strips Windows drive letters and normalizes
1058+ // backslashes to forward slashes.
9941059func normalizeForDocker (base , path string ) string {
9951060 // Convert backslashes to forward slashes first
9961061 base = strings .ReplaceAll (base , "\\ " , "/" )
@@ -1024,6 +1089,10 @@ func normalizeForDocker(base, path string) string {
10241089 }
10251090 return filepath .ToSlash (rel )
10261091}
1092+ // mergeMetadataWithFFmpeg combines audio from the converted temp file with
1093+ // metadata and cover art from the original source file using FFmpeg. On
1094+ // success the temp file is removed; if NoPreserveMetadata is set it simply
1095+ // renames the temp to the target path.
10271096func mergeMetadataWithFFmpeg (sourcePath , tempConvertedPath , targetPath string ) error {
10281097 if config .NoPreserveMetadata {
10291098 // If not preserving metadata, just rename temp to target
@@ -1074,6 +1143,8 @@ func mergeMetadataWithFFmpeg(sourcePath, tempConvertedPath, targetPath string) e
10741143 return nil
10751144}
10761145
1146+ // copyImageFiles walks the source directory and copies all JPG and PNG files
1147+ // to the target directory, preserving the directory structure.
10771148func copyImageFiles () error {
10781149 fmt .Println ("Copying image files..." )
10791150
@@ -1201,6 +1272,8 @@ func copyFile(src, dst string) error {
12011272 return doCopyFile (src , dst )
12021273}
12031274
1275+ // GitHubRelease represents a GitHub release API response, containing the
1276+ // tag name used for version comparison in self-update.
12041277type GitHubRelease struct {
12051278 TagName string `json:"tag_name"`
12061279}
@@ -1238,6 +1311,9 @@ func compareVersions(v1, v2 string) int {
12381311 return 0
12391312}
12401313
1314+ // selfUpdate checks the GitHub releases API for a newer version, downloads
1315+ // the matching platform archive, extracts the binary, and replaces the
1316+ // currently running executable with a backup fallback on failure.
12411317func selfUpdate (client * http.Client ) error {
12421318 currentVersion := version
12431319 if currentVersion == "dev" {
0 commit comments