package asses import ( "bytes" "context" "fmt" "log" "os" "os/exec" "path" "path/filepath" "regexp" "slices" "strings" "time" ) func Entrypoint(ctx context.Context, p string) error { return deport(ctx, p) } 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) } } if err := BestAssToSRT(ctx, p); err != nil { return err } base := path.Base(p) withoutExt := strings.TrimSuffix(base, path.Ext(base)) p2 := path.Join(path.Dir(p), fmt.Sprintf("%s.subless.mkv", withoutExt)) args := []string{ "-i", p, "-map", "0", } for _, assStream := range assStreams { args = append(args, "-map", "-"+assStream.id) } 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 } 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 } func BestAssToSRT(ctx context.Context, p string) error { asses, err := filepath.Glob(path.Join( path.Dir(p), fmt.Sprintf(".%s.*.ass", path.Base(p)), )) if err != nil { return err } srts := []string{} for _, ass := range asses { srt, err := assToSRT(ctx, ass) if err != nil { return err } srts = append(srts, srt) } srts = SRTsByGoodness(srts) 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 } } else { os.Remove(srts[i]) } } return nil } func assToSRT(ctx context.Context, ass string) (string, error) { ctx, can := context.WithTimeout(ctx, 30*time.Second) defer can() srt := fmt.Sprintf("%s.srt", strings.TrimSuffix(ass, ".ass")) if _, err := os.Stat(srt); err == nil { return srt, nil } if err := ffmpeg(ctx, "-y", "-i", ass, srt); err != nil { if ctx.Err() == nil { log.Printf("ffmpeg failed to process %s; removing", ass) os.Remove(ass) } return srt, err } b, err := os.ReadFile(srt) if err != nil { return srt, 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 srt, err } return srt, nil } func SRTsByGoodness(srts []string) []string { skippers := []*regexp.Regexp{ regexp.MustCompile(`(?i)lat.*amer`), regexp.MustCompile(`(?i)signs`), regexp.MustCompile(`(?i)rus`), regexp.MustCompile(`(?i)por`), regexp.MustCompile(`(?i)ita`), regexp.MustCompile(`(?i)fre`), regexp.MustCompile(`(?i)spa`), regexp.MustCompile(`(?i)ger`), regexp.MustCompile(`(?i)ara`), regexp.MustCompile(`(?i)jpn`), regexp.MustCompile(`(?i)urop`), regexp.MustCompile(`(?i)razil`), regexp.MustCompile(`(?i)Deu`), regexp.MustCompile(`(?i)ara`), } keepers := []*regexp.Regexp{ regexp.MustCompile(`(?i)^eng$`), } srts = slices.Clone(srts) slices.SortFunc(srts, func(a, b string) int { a = strings.ToLower(a) b = strings.ToLower(b) for _, skipper := range skippers { if skipper.MatchString(b) { return -1 } else if skipper.MatchString(a) { return 1 } } for _, keeper := range keepers { if keeper.MatchString(a) { return -1 } else if keeper.MatchString(b) { return 1 } } return strings.Compare(a, b) }) return srts }