package main import ( "bytes" "context" "encoding/json" "flag" "fmt" "io/fs" "io/ioutil" "log" "os" "os/signal" "path" "regexp" "strings" "syscall" "text/template" yaml "gopkg.in/yaml.v3" ) var ( Debug = os.Getenv("DEBUG") == "true" ConstTitle = os.Getenv("YAML_C_TITLE") ConstSeason = os.Getenv("YAML_C_SEASON") ConstEpisode = os.Getenv("YAML_C_EPISODE") ConstOutd = os.Getenv("YAML_O") Dry = os.Getenv("YAML_D") == "true" || os.Getenv("DRY") == "true" ConstPatterns = os.Getenv("YAML_P") ) type Yaml struct { C Fields O string D bool P []string } const YamlFile = ".show-ingestion.yaml" 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 } else if len(os.Args) == 2 && os.Args[1] == "i" { foo = Stage } if err := foo(ctx); err != nil { panic(err) } } func Stage(ctx context.Context) error { if _, err := os.Stat(YamlFile); err == nil { return nil } b, _ := yaml.Marshal(Yaml{ D: true, O: "/volume1/video/Bel/Anime/{{.Title}}/Season_{{.Season}}", }) return ioutil.WriteFile(YamlFile, b, os.ModePerm) } func Recursive(ctx context.Context) error { q := []string{"./"} for len(q) > 0 { d := q[0] q = q[1:] p := path.Join(d, YamlFile) if _, err := os.Stat(p); err != nil { } else if err := func() error { y, err := NewYaml(path.Join(d, YamlFile)) if err != nil { return err } was, err := os.Getwd() if err != nil { return err } if err := os.Chdir(d); err != nil { return err } defer os.Chdir(was) log.Printf("Run(outd=%s, ind=%s, patterns=%+v, const=%+v, dry=%v)", y.O, d, y.P, y.C, y.D) 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 template accepts overrides format title case") 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, ) } const ( PatternGroupTitleHyphenSEDual = `^(\[[^\]]*\] )?(?P.*?)( -)?[ \.](S(?P<season>[0-9]{2})E)?(?P<episode>[0-9]{2})[^0-9].*[dD][uU][aA][lL].*` PatternGroupTitleHyphenSE = `^(\[[^\]]*\] )?(?P<title>.*?)( -)?[ \.](S(?P<season>[0-9]{2})E)?(?P<episode>[0-9]{2})[^0-9].*` PatternTitleSEDual = `^(?P<title>.*) S(?P<season>[0-9]+)E(?P<episode>[0-9]+).*[dD][uU][aA][lL].*` PatternTitleSE = `^(?P<title>.*) S(?P<season>[0-9]+)E(?P<episode>[0-9]+).*` SEDual = `^S(?P<season>[0-9]+)E(?P<episode>[0-9]+).*[dD][uU][aA][lL].*` SE = `^S(?P<season>[0-9]+)E(?P<episode>[0-9]+).*` ) 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, PatternGroupTitleHyphenSEDual, PatternGroupTitleHyphenSE, PatternTitleSEDual, PatternTitleSE, SEDual, SE, ), 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 _, pattern := range patterns { for _, entry := range entries { if !entry.Type().IsRegular() && !(Debug && Dry) { continue } if err := one(ctx, outd, path.Join(ind, entry.Name()), []string{pattern}, 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 { found, match := Parse(f, pattern) if !match { continue } for _, wr := range [][2]*string{ [2]*string{&found.Title, &overrides.Title}, [2]*string{&found.Season, &overrides.Season}, [2]*string{&found.Episode, &overrides.Episode}, } { if *wr[1] != "" { *wr[0] = *wr[1] } } if found.Title == "" || found.Season == "" || found.Episode == "" { continue } found.Title = strings.ReplaceAll(found.Title, ".", " ") found.Title = strings.Join(strings.Fields(found.Title), "_") return foundOne(ctx, outd, inf, found, mvNLn) } return nil } func Parse(f string, pattern string) (Fields, bool) { re := regexp.MustCompile(pattern) if !re.MatchString(f) { return Fields{}, false } var found Fields 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]) } } return found, true } func foundOne(ctx context.Context, outd, inf string, fields Fields, mvNLn MvNLn) error { tmpl, err := template.New(inf).Parse(outd) if err != nil { return err } buff := bytes.NewBuffer(nil) if err := tmpl.Execute(buff, fields); err != nil { return err } outf := path.Join(string(buff.Bytes()), 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 { if Debug { fmt.Fprintf(os.Stderr, "no mv %q\n %q\n", inf, outf) } return nil } if _, ok := outd[outf]; ok { if Debug { fmt.Fprintf(os.Stderr, "no mv %q\n %q\n", inf, outf) } return nil } outd[outf] = struct{}{} fmt.Fprintf(os.Stderr, "mv %q\n %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 } func NewYaml(p string) (Yaml, error) { var y Yaml b, _ := os.ReadFile(p) if err := yaml.Unmarshal(b, &y); err != nil { return y, fmt.Errorf("%s: %w", p, err) } if ConstTitle != "" { y.C.Title = ConstTitle } if ConstSeason != "" { y.C.Season = ConstSeason } if ConstEpisode != "" { y.C.Episode = ConstEpisode } if ConstOutd != "" { y.O = ConstOutd } if Dry { y.D = true } if ConstPatterns != "" { y.P = strings.Split(ConstPatterns, ",") } return y, nil }