package tasks import ( "bufio" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/roemer/goext" ) type BlackBorderAnalyzeTask struct { logFilePath string cropDetectOutputRegex *regexp.Regexp } func NewBlackBorderAnalyzeTask(logFilePath string) *BlackBorderAnalyzeTask { return &BlackBorderAnalyzeTask{ logFilePath: logFilePath, cropDetectOutputRegex: regexp.MustCompile(`.*Parsed_cropdetect_0.* x1:([0-9]+) x2:([0-9]+) y1:([0-9]+) y2:([0-9]+) w:([0-9]+) h:([0-9]+) x:([0-9]+) y:([0-9]+) pts:([0-9]+) t:([0-9\.]+) limit:([0-9\.]+) crop=([0-9\:]+)`), } } func (t *BlackBorderAnalyzeTask) Run(folderPath string) error { files, err := os.ReadDir(folderPath) if err != nil { return fmt.Errorf("failed to read directory: %w", err) } for _, file := range files { // Skip directories if file.IsDir() { continue } // Skip reencoded files if strings.HasSuffix(file.Name(), "_2.mkv") { continue } t.output(fmt.Sprintf("=== %s", file.Name())) if err := t.processFile(filepath.Join(folderPath, file.Name())); err != nil { return fmt.Errorf("failed to process file %s: %w", file.Name(), err) } } return nil } func (t *BlackBorderAnalyzeTask) processFile(filePath string) error { // Get the resolution of the video resOutput, _, err := goext.CmdRunners.Default.RunGetOutput("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "json", filePath) if err != nil { return fmt.Errorf("failed to run ffprobe: %w", err) } decoder := json.NewDecoder(strings.NewReader(resOutput)) var result struct { Streams []struct { Width int `json:"width"` Height int `json:"height"` } `json:"streams"` } if err := decoder.Decode(&result); err != nil { return fmt.Errorf("failed to decode ffprobe output: %w", err) } videoWidth := result.Streams[0].Width videoHeight := result.Streams[0].Height // Execute a command and process output in real time cmd := exec.Command("ffmpeg", "-v", "debug", "-i", filePath, "-vf", "cropdetect", "-f", "null", "-") // Create a single pipe that combines both stdout and stderr combinedOutput, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("failed to create stdout pipe: %w", err) } // Redirect stderr to stdout so both streams are handled together cmd.Stderr = cmd.Stdout // Start the command if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start command: %w", err) } // Process combined output in real time with a single goroutine go func() { lastValue := &blackBorderAnalyzeData{} scanner := bufio.NewScanner(combinedOutput) for scanner.Scan() { line := scanner.Text() // Process each line of combined output here value := t.processOutputLine(line) if value != nil { if value.x1 != lastValue.x1 || value.x2 != lastValue.x2 || value.y1 != lastValue.y1 || value.y2 != lastValue.y2 || value.w != lastValue.w || value.h != lastValue.h || value.x != lastValue.x || value.y != lastValue.y { // output(value.String()) t.output(fmt.Sprintf("left=%d, right=%d, top=%d, bottom=%d, time:%s", value.x1, videoWidth-value.x2, value.y1, videoHeight-value.y2, value.timeStamp)) } lastValue = value } } if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "Error reading combined output: %v\n", err) } }() // Wait for the command to complete return cmd.Wait() } func (t *BlackBorderAnalyzeTask) processOutputLine(line string) *blackBorderAnalyzeData { match := t.cropDetectOutputRegex.FindStringSubmatch(line) if match != nil { //fmt.Println(line) // Extracted values can be processed further x1 := match[1] x2 := match[2] y1 := match[3] y2 := match[4] w := match[5] h := match[6] x := match[7] y := match[8] //pts := match[9] t := match[10] //limit := match[11] //crop := match[12] x1v, _ := strconv.Atoi(x1) x2v, _ := strconv.Atoi(x2) y1v, _ := strconv.Atoi(y1) y2v, _ := strconv.Atoi(y2) wv, _ := strconv.Atoi(w) hv, _ := strconv.Atoi(h) xv, _ := strconv.Atoi(x) yv, _ := strconv.Atoi(y) timeStamp, _ := strconv.ParseFloat(t, 64) return &blackBorderAnalyzeData{ x1: x1v, x2: x2v, y1: y1v, y2: y2v, w: wv, h: hv, x: xv, y: yv, timeStamp: time.Duration(timeStamp * float64(time.Second)), } } return nil } func (t *BlackBorderAnalyzeTask) output(value string) { fmt.Println(value) // Append to file f, err := os.OpenFile(t.logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Fprintf(os.Stderr, "Error opening file for writing: %v\n", err) return } defer f.Close() if _, err := f.WriteString(value + "\n"); err != nil { fmt.Fprintf(os.Stderr, "Error writing to file: %v\n", err) } } type blackBorderAnalyzeData struct { x1 int x2 int y1 int y2 int w int h int x int y int timeStamp time.Duration } func (c blackBorderAnalyzeData) String() string { return fmt.Sprintf("x1: %d, x2: %d, y1: %d, y2: %d, w: %d, h: %d, x: %d, y: %d, timeStamp: %s", c.x1, c.x2, c.y1, c.y2, c.w, c.h, c.x, c.y, c.timeStamp) }