package main import ( "bufio" "fmt" "io" "log" "net/http" "os" "os/exec" "regexp" "runtime" "strconv" "time" "github.com/gorilla/mux" ) func limitLength(s string, length int) string { if len(s) < length { return s } return s[:length] } // globals, huh, faster than channels and oddly more appropriate, we care less about races than feedback. var lastPercentage float64 var lastFilename string var lastDurationSeconds float64 func convertFile(sourceFileName string, destinationFilename string) error { launch := "./ffmpeg" if runtime.GOOS == "windows" { launch += ".exe" } os.Remove(destinationFilename) DurationRe := regexp.MustCompile(`DURATION *: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)`) OtherDurationRe := regexp.MustCompile(`Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+)`) FrameRe := regexp.MustCompile(`frame= ?([0-9]+) `) FPSValue := 0.0 finalFramesValue := 0.0 FPSRe := regexp.MustCompile(`, ([0-9\.]+) fps,`) fmt.Printf("%s %s %s %s %s %s %s %s %s %s %s %s %s %s %s", launch, "-loglevel", "repeat+level+verbose", "-i", sourceFileName, "-c:v", "libvpx", "-crf", "10", "-b:v", "1M", "-c:a", "libvorbis", "-y", destinationFilename) cmd := exec.Command(launch, "-progress", "pipe:2", "-i", sourceFileName, "-c:v", "libvpx", "-crf", "10", "-b:v", "1M", "-c:a", "libvorbis", "-y", destinationFilename) stdout, err := cmd.StderrPipe() if err != nil { log.Fatal(err) } if err := cmd.Start(); err != nil { log.Fatal(err) } // Start reading from the file with a reader. reader := bufio.NewReader(stdout) var line string for { line, err = reader.ReadString('\n') if err != nil && err != io.EOF { break } fpses := FPSRe.FindAllStringSubmatch(line, 1) if len(fpses) > 0 { fmt.Printf("FPS' is %v\n", fpses) FPSValue, _ = strconv.ParseFloat(fpses[0][1], 64) fmt.Printf("\nSet fps to %0.2f\n", FPSValue) if lastDurationSeconds != 0 { finalFramesValue = lastDurationSeconds * FPSValue } } durations := DurationRe.FindAllStringSubmatch(line, 1) if len(durations) < 1 { durations = OtherDurationRe.FindAllStringSubmatch(line, 1) } if len(durations) > 0 { fmt.Printf("Durations is %v\n", durations) // we have an fps, we can work out max frames! hrs, _ := strconv.ParseFloat(durations[0][1], 64) mins, _ := strconv.ParseFloat(durations[0][2], 64) secs, _ := strconv.ParseFloat(durations[0][3], 64) aaand, _ := strconv.ParseFloat("0."+durations[0][4], 64) lastDurationSeconds = (hrs * 60 * 60) + (mins * 60) + secs + aaand if FPSValue != 0.0 { finalFramesValue = lastDurationSeconds * FPSValue } fmt.Printf("%0f:%0f:%0f%0.4f * %0.2f = %0.2f seconds = %0.2f frames\n", hrs, mins, secs, aaand, FPSValue, lastDurationSeconds, finalFramesValue) } currentFrames := FrameRe.FindAllStringSubmatch(line, 1) if len(currentFrames) > 0 { currentFrame, _ := strconv.ParseFloat(currentFrames[0][1], 64) percentage := currentFrame / finalFramesValue * 100 lastPercentage = (percentage / 2) + 50 fmt.Printf("Frame %0f / %0f = Percent : %0.2f\n", currentFrame, finalFramesValue, lastPercentage) } if len(fpses) == 0 && len(durations) == 0 && len(currentFrames) == 0 { //fmt.Printf("X: %s", line) } if err != nil { break } } if err != io.EOF { fmt.Printf(" > Failed with error: %v\n", err) return err } lastPercentage = 100 fmt.Printf("Setting to %0f percent.\n", lastPercentage) os.Remove(sourceFileName) go func() { time.Sleep(10 * time.Second) lastPercentage = 0 }() return nil } func downloadFile(sourceURL string, outputFile string, statusChannel chan float64, nameChannel chan string) error { progressBackwards := false lastLastPercentage := 0.0 launch := "./youtube-dl" if runtime.GOOS == "windows" { launch += ".exe" } fmt.Printf("%s %s %s %s %s\n", launch, sourceURL, "-o", outputFile, "--newline") cmd := exec.Command(launch, sourceURL, "-o", outputFile, "--newline") stdout, err := cmd.StdoutPipe() if err != nil { log.Fatal(err) } if err := cmd.Start(); err != nil { log.Fatal(err) } re := regexp.MustCompile(`([0-9.]+)%`) reName := regexp.MustCompile(`Merging formats into "([^"]+)"`) // Start reading from the file with a reader. reader := bufio.NewReader(stdout) var line string for { line, err = reader.ReadString('\n') if err != nil && err != io.EOF { break } percentages := re.FindAllStringSubmatch(line, 1) if len(percentages) > 0 { p, err := strconv.ParseFloat(string(percentages[0][1]), 64) if err != nil { fmt.Printf("What kind of number is %v", percentages[0][1]) } else { if lastLastPercentage > p { progressBackwards = true } lastLastPercentage = p if progressBackwards { // youtube-dl does video, then audio - it's not really half but an estimation lastPercentage = float64(25) + p/4 } else { lastPercentage = p / 4 } statusChannel <- lastPercentage } } fmt.Printf("%s\n", line) if err != nil { break } filenames := reName.FindAllStringSubmatch(line, 1) if len(filenames) > 0 { fmt.Printf("Filename: %v", filenames) lastFilename = filenames[0][1] nameChannel <- filenames[0][1] } } if err := cmd.Wait(); err != nil { log.Fatal(err) } if err != io.EOF { fmt.Printf(" > Failed with error: %v\n", err) return err } if lastFilename == "" { // we probably didn't get a merge into a single file, so it probably just streamed to the output without an extension lastFilename = "test" } return convertFile(lastFilename, "converted.webm") } func HomeHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Hello %s\n", "world") } var c chan float64 var n chan string func FetchHandler(w http.ResponseWriter, r *http.Request) { //vars := mux.Vars(r) w.WriteHeader(http.StatusOK) if lastFilename != "" { os.Remove(lastFilename) } go downloadFile(r.FormValue("url"), "test", c, n) fmt.Fprint(w, "OK\n") } func PidHandler(w http.ResponseWriter, r *http.Request) { //vars := mux.Vars(r) w.WriteHeader(http.StatusOK) if lastFilename != "" { os.Remove(lastFilename) } fmt.Fprintf(w, "%d\n", os.Getpid()) } func QuitHandler(w http.ResponseWriter, r *http.Request) { //vars := mux.Vars(r) w.WriteHeader(http.StatusOK) if lastFilename != "" { os.Remove(lastFilename) } fmt.Fprint(w, "BYE!\n") go func() { time.Sleep(2 * time.Second) os.Exit(0) }() } func StatusHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "application/json") w.WriteHeader(http.StatusOK) // Oddly, using channels here is slower and we don't actually care if there's a race condition. /* if len(c) > 0 { lastPercentage = <-c } if len(n) > 0 { lastFilename = <-n } */ fmt.Printf("Returning %0.2f for %s\n", lastPercentage, lastFilename) fmt.Fprintf(w, `{"filename": "%s","percentage":%0.2f,"duration_seconds":%0.6f}`+"\n", lastFilename, lastPercentage, lastDurationSeconds) } func main() { if len(os.Args) > 0 { os.Chdir(os.Args[1]) } r := mux.NewRouter() r.SkipClean(true) r.Path("/get").Queries("url", "{.*}").HandlerFunc(FetchHandler) r.HandleFunc("/status/", StatusHandler) r.HandleFunc("/pid/", PidHandler) r.HandleFunc("/quit/", QuitHandler) r.HandleFunc("/", HomeHandler) http.Handle("/", r) c = make(chan float64, 999) n = make(chan string, 999) srv := &http.Server{ Handler: r, Addr: "127.0.0.1:10435", // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Fatal(srv.ListenAndServe()) }