398 lines
9.4 KiB
Go
398 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"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")
|
|
WebhookOnRecursiveMiss = os.Getenv("RECURSIVE_MISSING_WEBHOOK")
|
|
WebhookOnRecursiveMissCacheD = os.Getenv("RECURSIVE_MISSING_WEBHOOK_CACHE_D")
|
|
)
|
|
|
|
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 {
|
|
log.Printf("%s has no %s", d, YamlFile)
|
|
if WebhookOnRecursiveMiss != "" && WebhookOnRecursiveMissCacheD != "" {
|
|
cacheP := regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(p, `_`)
|
|
cacheP = path.Join(WebhookOnRecursiveMissCacheD, cacheP)
|
|
if _, err := os.Stat(cacheP); err != nil {
|
|
req, err := http.NewRequest(http.MethodPut, WebhookOnRecursiveMiss, strings.NewReader(p))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
u, err := url.Parse(WebhookOnRecursiveMiss)
|
|
if err != nil {
|
|
return fmt.Errorf("WebhookOnRecursiveMiss (%s) invalid: %w", WebhookOnRecursiveMiss, err)
|
|
}
|
|
user := u.User
|
|
u.User = nil
|
|
if username := user.Username(); username != "" {
|
|
password, _ := user.Password()
|
|
req.SetBasicAuth(username, password)
|
|
}
|
|
req.URL = u
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to call %s for missing %s: %w", WebhookOnRecursiveMiss, p, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
defer io.Copy(io.Discard, resp.Body)
|
|
if resp.StatusCode > 250 {
|
|
return fmt.Errorf("unexpected status code from %s for %s: %w", WebhookOnRecursiveMiss, p, err)
|
|
}
|
|
|
|
os.MkdirAll(path.Dir(cacheP), os.ModePerm)
|
|
ioutil.WriteFile(cacheP, []byte{}, os.ModePerm)
|
|
}
|
|
}
|
|
} 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<title>.*?)( -)?[ \.](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
|
|
}
|
|
done := map[int]bool{}
|
|
for _, pattern := range patterns {
|
|
for i, entry := range entries {
|
|
if done[i] {
|
|
continue
|
|
}
|
|
if !entry.Type().IsRegular() && !(Debug && Dry) {
|
|
continue
|
|
}
|
|
if match, err := one(ctx, outd, path.Join(ind, entry.Name()), []string{pattern}, overrides, mvNLn); err != nil {
|
|
return err
|
|
} else if match {
|
|
done[i] = true
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func one(ctx context.Context, outd, inf string, patterns []string, overrides Fields, mvNLn MvNLn) (bool, error) {
|
|
f := path.Base(inf)
|
|
for _, pattern := range patterns {
|
|
found, match := Parse(f, pattern)
|
|
if !match {
|
|
if Debug {
|
|
log.Printf("%q does not match %q", pattern, f)
|
|
}
|
|
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 == "" {
|
|
if Debug {
|
|
log.Printf("%q does not match all %q: %+v", pattern, f, found)
|
|
}
|
|
continue
|
|
}
|
|
found.Title = strings.ReplaceAll(found.Title, ".", " ")
|
|
found.Title = strings.Join(strings.Fields(found.Title), "_")
|
|
|
|
if Debug {
|
|
log.Printf("%q matches %q as %+v", pattern, f, found)
|
|
}
|
|
return true, foundOne(ctx, outd, inf, found, mvNLn)
|
|
}
|
|
return false, 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
|
|
}
|