package transcoder import ( "bufio" "encoding/json" "errors" "fmt" "math" "os" "os/exec" "strconv" "strings" "time" "gitlab.com/ChaotiCryptidz/musicutil/types" ) type TranscodeConfig struct { UseQuality bool `json:"use_quality"` UseBitrate bool `json:"use_bitrate"` Encoder string `json:"encoder"` FileExtension string `json:"file_extension"` Container string `json:"container"` Bitrate string `json:"bitrate"` Quality string `json:"quality"` SampleRate string `json:"sample_rate"` Channels string `json:"channels"` } func isNotEmptyString(x string) bool { if len(strings.TrimSpace(x)) == 0 { return false } return true } func progressDurationToMilliSeconds(duration string) int64 { fields := strings.Split(strings.TrimSpace(duration), ":") durationNs := int64(0) h, _ := strconv.ParseInt(fields[0], 10, 64) durationNs += int64(time.Hour) * h m, _ := strconv.ParseInt(fields[1], 10, 64) durationNs += int64(time.Minute) * m s, _ := strconv.ParseFloat(fields[2], 64) durationNs += int64(float64(time.Second) * s) return time.Duration(durationNs).Milliseconds() } func getFileLengthMilliSeconds(filename string) (int64, error) { type FFProbeFormat struct { Duration string `json:"duration"` } type FFProbeOutput struct { Format FFProbeFormat `json:"format"` } ffprobe_output_str, err := exec.Command( "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", filename, ).Output() if err != nil { return 0, err } var ffprobe_output FFProbeOutput err = json.Unmarshal([]byte(ffprobe_output_str), &ffprobe_output) if err != nil { return 0, err } else { duration_str := ffprobe_output.Format.Duration duration_float, err := strconv.ParseFloat(duration_str, 64) if err != nil { return 0, err } duration_millis := int64(math.Round(duration_float * 1000)) return duration_millis, nil } } func Transcode(file *types.File, config *TranscodeConfig, progress_chan chan string, dest string) (string, error) { command_args := make([]string, 0) command_args = append(command_args, "-y") command_args = append(command_args, "-hide_banner") command_args = append(command_args, "-i") command_args = append(command_args, file.JoinPathTo()) if isNotEmptyString(config.Encoder) { command_args = append(command_args, "-c:a") command_args = append(command_args, config.Encoder) } if isNotEmptyString(config.Container) { command_args = append(command_args, "-f") command_args = append(command_args, config.Container) } if isNotEmptyString(config.SampleRate) { command_args = append(command_args, "-ar") command_args = append(command_args, config.SampleRate) } if isNotEmptyString(config.Channels) { command_args = append(command_args, "-ac") command_args = append(command_args, config.Channels) } if config.UseQuality { command_args = append(command_args, "-q:a") command_args = append(command_args, config.Quality) } if config.UseBitrate { command_args = append(command_args, "-b:a") command_args = append(command_args, config.Bitrate) } command_args = append(command_args, dest) // Progress Shenanigans if progress_chan != nil { total_length_milliseconds, err := getFileLengthMilliSeconds(file.JoinPathTo()) if err != nil { goto end } progress_temp_dir, err := os.MkdirTemp("", "*-musicutil_transcode_temp") if err != nil { goto end } defer os.RemoveAll(progress_temp_dir) progress_filename := progress_temp_dir + "/progress.log" progress_file, err := os.Create(progress_filename) if err != nil { goto end } defer os.Remove(progress_filename) command_args = append(command_args, "-progress", progress_filename, "-nostats") go func() { reader := bufio.NewReader(progress_file) for { line, err := reader.ReadBytes('\n') if err == nil { if strings.HasPrefix(string(line), "out_time=") { out_time := strings.TrimSuffix(strings.TrimPrefix(string(line), "out_time="), "\n") out_time_ms := progressDurationToMilliSeconds(out_time) progress_chan <- fmt.Sprintf("%0.2f%%", (float64(out_time_ms) / float64(total_length_milliseconds) * 100)) } } } }() } end: cmd := exec.Command("ffmpeg", command_args...) output, err := cmd.CombinedOutput() if err != nil { fmt.Println(output) return string(output), errors.New("ffmpeg error") } if progress_chan != nil { close(progress_chan) } return string(output), nil }