package asses import ( "bytes" "context" "fmt" "log" "os" "os/exec" "path" "path/filepath" "regexp" "slices" "strings" ) func deport(ctx context.Context, p string) error { if os.Getenv("NO_DEPORT") != "" { log.Printf("would deport %s", p) return nil } assStreams, err := assStreams(ctx, p) if err != nil { return err } assStreamIDs := make([]string, len(assStreams)) for i, stream := range assStreams { assStreamIDs[i] = stream.id assF := path.Join( path.Dir(p), fmt.Sprintf( ".%s.%s.%s.ass", path.Base(p), stream.id, stream.title, ), ) if err := ffmpeg(ctx, "-y", "-i", p, "-map", stream.id, assF); err != nil { return fmt.Errorf("failed to pull %s from %s: %w", stream.id, p, err) } } asses, err := filepath.Glob(path.Join( path.Dir(p), fmt.Sprintf(".%s.*.ass", path.Base(p)), )) if err != nil { return err } for _, ass := range asses { srt := fmt.Sprintf("%s.srt", strings.TrimSuffix(ass, ".ass")) if _, err := os.Stat(srt); err == nil { continue } if err := ffmpeg(ctx, "-y", "-i", ass, srt); err != nil { return err } b, err := os.ReadFile(srt) if err != nil { return err } before := len(b) b = regexp.MustCompile(`size="[^"]*"`).ReplaceAll(b, []byte{}) if after := len(b); before == after { } else if err := os.WriteFile(srt, b, os.ModePerm); err != nil { return err } } srts, err := filepath.Glob(path.Join( path.Dir(p), fmt.Sprintf(".%s.*.srt", path.Base(p)), )) if err != nil { return err } slices.SortFunc(srts, func(a, b string) int { // if skip a { return 1 } // if skip b { return -1 } // return -1 * (wc(a) - wc(b)) return strings.Compare(a, b) }) for i := range srts { if i == 0 { base := path.Base(p) withoutExt := strings.TrimSuffix(base, path.Ext(base)) srt := path.Join(path.Dir(p), fmt.Sprintf("%s.srt", withoutExt)) if err := os.Rename(srts[i], srt); err != nil { return err } p2 := path.Join(path.Dir(p), fmt.Sprintf("%s.subless.mkv", withoutExt)) args := []string{ "-i", p, "-map", "0", } for _, assStreamID := range assStreamIDs { args = append(args, "-map", "-"+assStreamID) } args = append(args, "-c", "copy", p2, ) if err := ffmpeg(ctx, args...); err != nil { return err } else if err := os.Rename(p2, p); err != nil { return err } } else { os.Remove(srts[i]) } } return nil } type stream struct { id string title string } func assStreams(ctx context.Context, p string) ([]stream, error) { output, err := ffprobe(ctx, "-i", p) if err != nil { return nil, err } result := []stream{} for _, line := range strings.Split(output, "\n") { fields := strings.Fields(line) if len(fields) < 3 { continue } else if fields[0] != "Stream" { continue } else if !strings.Contains(fields[1], "(") { continue } else if fields[2] != "Subtitle:" { continue } else if fields[3] != "ass" { continue } field1 := fields[1] id := strings.Trim(strings.Split(field1, "(")[0], "#") title := strings.Trim(strings.Split(field1, "(")[1], "):") result = append(result, stream{ id: id, title: title, }) } return result, nil } func ffprobe(ctx context.Context, args ...string) (string, error) { return execc(ctx, "ffprobe", args...) } func ffmpeg(ctx context.Context, args ...string) error { std, err := execc(ctx, "ffmpeg", args...) if err != nil { return fmt.Errorf("(%w) %s", err, std) } return nil } func execc(ctx context.Context, bin string, args ...string) (string, error) { stdout := bytes.NewBuffer(nil) cmd := exec.CommandContext(ctx, bin, args...) cmd.Stdin = nil cmd.Stderr = stdout cmd.Stdout = stdout err := cmd.Run() return string(stdout.Bytes()), err }