package main import ( "context" "encoding/json" "flag" "fmt" "io" "os" "os/signal" "path" "regexp" "strings" "syscall" ) type Fields struct { Title string Season string Episode string } type MvNLn func(string, string) error func main() { ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT) defer can() if len(os.Args) < 2 { if err := Recursive(ctx); err != nil { panic(err) } } else { if err := Main(ctx); err != nil { panic(err) } } } func Recursive(ctx context.Context) error { return io.EOF } func Main(ctx context.Context) error { flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) overridesS := flags.String("c", `{"title":"","season":"","episode":""}`, "overrides") ind := flags.String("i", "/dev/null", "in dir") outd := flags.String("o", "/dev/null", "out dir") dry := flags.Bool("d", true, "dry run") if err := flags.Parse(os.Args[1:]); err != nil { panic(err) } var overrides Fields json.Unmarshal([]byte(*overridesS), &overrides) mvNLn := RealMvNLn if *dry { mvNLn = DryMvNLn() } return Run(ctx, *outd, *ind, append(flags.Args(), `^\[[^\]]*\] (?P.*) - (?P<episode>[0-9]+) .*`, ), overrides, mvNLn, ) } func Run(ctx context.Context, outd, ind string, patterns []string, overrides Fields, mvNLn MvNLn) error { entries, err := os.ReadDir(ind) if err != nil { return err } for _, entry := range entries { if !entry.Type().IsRegular() { continue } if err := one(ctx, outd, path.Join(ind, entry.Name()), patterns, overrides, mvNLn); err != nil { return err } } return nil } func one(ctx context.Context, outd, inf string, patterns []string, overrides Fields, mvNLn MvNLn) error { f := path.Base(inf) for _, pattern := range patterns { re := regexp.MustCompile(pattern) if !re.MatchString(f) { continue } found := overrides groupNames := re.SubexpNames() groups := re.FindStringSubmatch(f) for i := 1; i < len(groupNames); i++ { v := groups[i] switch groupNames[i] { case "title": found.Title = v case "season": found.Season = v case "episode": found.Episode = v default: return fmt.Errorf("unexpected capture group %q", groupNames[i]) } } if found.Title == "" || found.Season == "" || found.Episode == "" { continue } found.Title = strings.Join(strings.Fields(found.Title), "_") return foundOne(ctx, outd, inf, found, mvNLn) } return nil } func foundOne(ctx context.Context, outd, inf string, fields Fields, mvNLn MvNLn) error { outf := path.Join(outd, fmt.Sprintf("%s_S%sE%s%s", fields.Title, fields.Season, fields.Episode, path.Ext(inf))) return mvNLn(outf, inf) } func RealMvNLn(outf, inf string) error { if stat, err := os.Stat(inf); err != nil || !stat.Mode().IsRegular() { return fmt.Errorf("cannot mv_n_ln(%s): (%v) mode=%v", inf, err, stat.Mode()) } if _, err := os.Stat(outf); err == nil { return nil // fmt.Errorf("conflict: %s already exists", path.Base(outf)) } if err := os.Rename(inf, outf); err != nil { return err } return os.Symlink(outf, inf) } func DryMvNLn() func(string, string) error { outd := map[string]struct{}{} return func(outf, inf string) error { if _, err := os.Stat(outf); err == nil { return nil } if _, ok := outd[outf]; ok { return nil } outd[outf] = struct{}{} fmt.Fprintf(os.Stderr, "mv %q %q\n", inf, outf) return nil } }