package main import ( "context" "encoding/json" "flag" "fmt" "io/fs" "os" "os/signal" "path" "regexp" "strings" "syscall" yaml "gopkg.in/yaml.v3" ) 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() foo := Main if len(os.Args) == 2 && os.Args[1] == "r" { foo = Recursive } if err := foo(ctx); err != nil { panic(err) } } func Recursive(ctx context.Context) error { q := []string{"./"} for len(q) > 0 { d := q[0] q = q[1:] p := path.Join(d, ".show-ingestion.yaml") if _, err := os.Stat(p); err != nil { } else if err := func() error { var y struct { C Fields O string D bool P []string } b, _ := os.ReadFile(path.Join(d, ".show-ingestion.yaml")) if err := yaml.Unmarshal(b, &y); err != nil { return fmt.Errorf("%s: %w", p, err) } was, err := os.Getwd() if err != nil { return err } if err := os.Chdir(d); err != nil { return err } defer os.Chdir(was) if err := Run(ctx, y.O, "./", y.P, y.C, y.D); err != nil { return err } return os.Chdir(was) }(); err != nil { return fmt.Errorf("%s: %w", p, err) } entries, err := readDir(d) if err != nil { return err } for _, entry := range entries { if entry.IsDir() { q = append(q, path.Join(d, entry.Name())) } } } return nil } 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) return Run(ctx, *outd, *ind, flags.Args(), overrides, *dry, ) } func Run(ctx context.Context, outd, ind string, patterns []string, overrides Fields, dry bool) error { mvNLn := RealMvNLn if dry { mvNLn = DryMvNLn() } return RunWith(ctx, outd, ind, append(patterns, `^\[[^\]]*\] (?P.*) - (?P<episode>[0-9]+).*`, `^(?P<title>.*) S(?P<season>[0-9]+)E(?P<episode>[0-9]+).*`, ), overrides, mvNLn, ) } func RunWith(ctx context.Context, outd, ind string, patterns []string, overrides Fields, mvNLn MvNLn) error { entries, err := 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 } if err := os.MkdirAll(path.Dir(outf), os.ModePerm); err != nil { return err } 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 } } func readDir(d string) ([]fs.DirEntry, error) { entries, err := os.ReadDir(d) result := []fs.DirEntry{} for _, entry := range entries { if !strings.HasPrefix(entry.Name(), ".") { result = append(result, entry) } } return result, err }