Compare commits
35 Commits
bff2bd6d2e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad5803297 | ||
|
|
2e4e4b9b06 | ||
|
|
61f9b9c724 | ||
|
|
5b9bead96f | ||
|
|
54bbca8fea | ||
|
|
3e8e33816e | ||
|
|
00fdd4f976 | ||
|
|
1e3d6ee0a2 | ||
|
|
d3c9b1a564 | ||
|
|
e653c35275 | ||
|
|
596865ede3 | ||
|
|
62ea963807 | ||
|
|
82a13aea65 | ||
|
|
d40a1f8fd4 | ||
|
|
e85fec9bbf | ||
|
|
5f5015e152 | ||
|
|
12bb9c808b | ||
|
|
984b53c6f1 | ||
|
|
57d9b74c31 | ||
|
|
d57206357e | ||
|
|
4c32fb0411 | ||
|
|
daa520de7d | ||
|
|
6af1f231df | ||
|
|
8185311a4f | ||
|
|
da9e0942fe | ||
|
|
1d5a7940d3 | ||
|
|
4738a9da0a | ||
|
|
97d75d5d59 | ||
|
|
54f67d6d77 | ||
|
|
caef12deb8 | ||
|
|
dc8c9fdf0c | ||
|
|
805e666230 | ||
|
|
81d1ce1dde | ||
|
|
b696251895 | ||
|
|
61569b0515 |
2
go.mod
2
go.mod
@@ -1,3 +1,5 @@
|
||||
module gitea/show-ingestion
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
3
go.sum
Normal file
3
go.sum
Normal file
@@ -0,0 +1,3 @@
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
382
main.go
382
main.go
@@ -1,20 +1,398 @@
|
||||
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()
|
||||
|
||||
if err := Run(ctx); err != nil {
|
||||
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 Run(ctx context.Context) error {
|
||||
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 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("unexpected status code from %s for %s: (%d) %s", WebhookOnRecursiveMiss, p, resp.StatusCode, b)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
402
main_test.go
402
main_test.go
@@ -2,13 +2,411 @@ package main_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
main "gitea/show-ingestion"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
if err := main.Run(context.Background()); err != nil {
|
||||
func TestRunChoosesOne(t *testing.T) {
|
||||
ind := t.TempDir()
|
||||
outd := t.TempDir()
|
||||
|
||||
for _, given := range []string{
|
||||
"Survivor.AU.S12E11.1080p.HEVC.x265-MeGusta[EZTVx.to].mkv",
|
||||
"Survivor.AU.S12E11.720p.HEVC.x265-MeGusta[EZTVx.to].mkv",
|
||||
"Survivor.AU.S12E12.720p.HEVC.x265-MeGusta[EZTVx.to].mkv",
|
||||
} {
|
||||
ioutil.WriteFile(path.Join(ind, given), []byte{}, os.ModePerm)
|
||||
}
|
||||
|
||||
want := map[string]bool{
|
||||
"Australian_Survivor_S12E11.mkv": false,
|
||||
"Australian_Survivor_S12E12.mkv": false,
|
||||
}
|
||||
|
||||
if err := main.RunWith(context.Background(),
|
||||
outd,
|
||||
ind,
|
||||
[]string{
|
||||
".urvivor.[Aa][Uu].*[sS](?P<season>[0-9]+)[eE](?P<episode>[0-9]*).*1080.*MeGusta",
|
||||
".urvivor.[Aa][Uu].*[sS](?P<season>[0-9]+)[eE](?P<episode>[0-9]*).*720.*MeGusta",
|
||||
},
|
||||
main.Fields{
|
||||
Title: "Australian_Survivor",
|
||||
},
|
||||
func(outf, inf string) error {
|
||||
want[path.Base(outf)] = true
|
||||
return nil
|
||||
},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for k, v := range want {
|
||||
if !v {
|
||||
t.Errorf("did not mv_n_ln(outf=%s)", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWith(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
given []string
|
||||
patterns []string
|
||||
overrides main.Fields
|
||||
want []string
|
||||
}{
|
||||
"empty": {},
|
||||
"fallback survivor": {
|
||||
given: []string{
|
||||
"Survivor.AU.S12E11.1080p.HEVC.x265-MeGusta[EZTVx.to].mkv",
|
||||
"Survivor.AU.S12E11.720p.HEVC.x265-MeGusta[EZTVx.to].mkv",
|
||||
"Survivor.AU.S12E12.720p.HEVC.x265-MeGusta[EZTVx.to].mkv",
|
||||
},
|
||||
patterns: []string{
|
||||
".urvivor.[Aa][Uu].*[sS](?P<season>[0-9]+)[eE](?P<episode>[0-9]*).*1080.*MeGusta",
|
||||
".urvivor.[Aa][Uu].*[sS](?P<season>[0-9]+)[eE](?P<episode>[0-9]*).*720.*MeGusta",
|
||||
},
|
||||
overrides: main.Fields{
|
||||
Title: "Australian_Survivor",
|
||||
},
|
||||
want: []string{
|
||||
"Australian_Survivor_S12E11.mkv",
|
||||
"Australian_Survivor_S12E12.mkv",
|
||||
},
|
||||
},
|
||||
"hard w group": {
|
||||
given: []string{
|
||||
"[Yameii] Dr. Stone - S04E12 [English Dub] [CR WEB-DL 720p] [F6EF1948].mkv",
|
||||
},
|
||||
patterns: []string{
|
||||
main.PatternGroupTitleHyphenSE,
|
||||
},
|
||||
want: []string{
|
||||
"Dr_Stone_S04E12.mkv",
|
||||
},
|
||||
},
|
||||
"easy w group": {
|
||||
given: []string{
|
||||
"[SubsPlease] Tokidoki Bosotto Russia-go de Dereru Tonari no Alya-san - 01 (720p) [A12844D5].mkv",
|
||||
"[SubsPlease] Tokidoki Bosotto Russia-go de Dereru Tonari no Alya-san - 02 (720p) [2608F490].mkv",
|
||||
},
|
||||
patterns: []string{
|
||||
`^\[[^\]]*\] (?P<title>.*) - (?<episode>[0-9]*)`,
|
||||
},
|
||||
overrides: main.Fields{
|
||||
Season: "01",
|
||||
},
|
||||
want: []string{
|
||||
"Tokidoki_Bosotto_Russia-go_de_Dereru_Tonari_no_Alya-san_S01E01.mkv",
|
||||
"Tokidoki_Bosotto_Russia-go_de_Dereru_Tonari_no_Alya-san_S01E02.mkv",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ind := t.TempDir()
|
||||
for _, f := range c.given {
|
||||
ioutil.WriteFile(path.Join(ind, f), []byte{}, os.ModePerm)
|
||||
}
|
||||
outd := t.TempDir()
|
||||
|
||||
if err := main.RunWith(context.Background(), outd, ind, c.patterns, c.overrides, main.RealMvNLn); err != nil {
|
||||
t.Fatal("err on first run:", err)
|
||||
} else if err := main.RunWith(context.Background(), outd, ind, c.patterns, c.overrides, main.RealMvNLn); err != nil {
|
||||
t.Fatal("err on second run:", err)
|
||||
}
|
||||
|
||||
for _, f := range c.want {
|
||||
if stat, err := os.Stat(path.Join(outd, f)); os.IsNotExist(err) {
|
||||
t.Errorf("expected %s", f)
|
||||
} else if !stat.Mode().IsRegular() {
|
||||
t.Errorf("%s not a regular file: %v", f, stat.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
if entries, err := os.ReadDir(outd); err != nil {
|
||||
t.Error("failed to list outdir: %w", err)
|
||||
} else {
|
||||
for _, entry := range entries {
|
||||
t.Logf("%s", entry.Name())
|
||||
if !slices.Contains(c.want, path.Base(entry.Name())) {
|
||||
t.Errorf("unexpected %s", entry.Name())
|
||||
}
|
||||
if !entry.Type().IsRegular() {
|
||||
t.Errorf("non-regular file %s in out: %v", entry.Name(), entry.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if entries, err := os.ReadDir(ind); err != nil {
|
||||
t.Error("failed to list indir: %w", err)
|
||||
} else {
|
||||
for _, entry := range entries {
|
||||
inf := path.Join(ind, entry.Name())
|
||||
if _, err := os.Stat(inf); err != nil {
|
||||
t.Errorf("%s no longer in ind: %v", inf, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecursive(t *testing.T) {
|
||||
webhooks := []string{}
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("unexpected webhook method %s", r.Method)
|
||||
}
|
||||
if r.URL.User.String() != "" {
|
||||
t.Errorf("unexpected auth on url %s", r.URL.String())
|
||||
}
|
||||
if u, p, _ := r.BasicAuth(); u != "u" || p != "p" {
|
||||
t.Errorf("webhook didnt translate u:p to basic auth")
|
||||
}
|
||||
b, _ := ioutil.ReadAll(r.Body)
|
||||
t.Logf("%s { %s }", r.URL.String(), b)
|
||||
webhooks = append(webhooks, string(b))
|
||||
}))
|
||||
t.Cleanup(s.Close)
|
||||
t.Cleanup(func() {
|
||||
t.Logf("webhooks: %+v", webhooks)
|
||||
if len(webhooks) == 0 {
|
||||
t.Errorf("expected webhook calls but got none")
|
||||
}
|
||||
deduped := slices.Clone(webhooks)
|
||||
slices.Sort(deduped)
|
||||
slices.Compact(deduped)
|
||||
if len(deduped) != len(webhooks) {
|
||||
t.Errorf("expected no duplicate webhooks but got %+v", webhooks)
|
||||
}
|
||||
})
|
||||
|
||||
u, _ := url.Parse(s.URL)
|
||||
u.User = url.UserPassword("u", "p")
|
||||
main.WebhookOnRecursiveMiss = u.String()
|
||||
main.WebhookOnRecursiveMissCacheD = t.TempDir()
|
||||
t.Cleanup(func() {
|
||||
main.WebhookOnRecursiveMiss = ""
|
||||
main.WebhookOnRecursiveMissCacheD = ""
|
||||
})
|
||||
|
||||
was, _ := os.Getwd()
|
||||
t.Cleanup(func() { os.Chdir(was) })
|
||||
os.Chdir(t.TempDir())
|
||||
|
||||
outd := t.TempDir()
|
||||
os.MkdirAll(path.Join(outd, "A"), os.ModePerm)
|
||||
|
||||
// use config
|
||||
write("./showA/.show-ingestion.yaml", `{
|
||||
"c": {
|
||||
"title": "A",
|
||||
"season": "A",
|
||||
"episode": "A"
|
||||
},
|
||||
"p": [".*"],
|
||||
"o": "`+outd+`/A"
|
||||
}`)
|
||||
write("./showA/file.a")
|
||||
|
||||
// parse files and const wins
|
||||
write("./showB/.show-ingestion.yaml", `{
|
||||
"o": "`+outd+`/B_{{.Title}}_{{.Season}}_{{.Episode}}",
|
||||
"p": [],
|
||||
"c": {"title": "TITLE"}
|
||||
}`)
|
||||
write("./showB/title S01E02.b")
|
||||
|
||||
// use file pattern
|
||||
write("./dirA/showC/.show-ingestion.yaml", `{
|
||||
"o": "`+outd+`/C",
|
||||
"p": ["^(?P<title>.) (?P<season>.) (?P<episode>.)"]
|
||||
}`)
|
||||
write("./dirA/showC/t s e.c")
|
||||
|
||||
// dry run
|
||||
write("./dirA/showD/.show-ingestion.yaml", `{
|
||||
"o": "`+outd+`/D",
|
||||
"d": true
|
||||
}`)
|
||||
write("./dirA/showD/title S02E04.d")
|
||||
|
||||
// not configured
|
||||
os.MkdirAll("./dirB/showE", os.ModePerm)
|
||||
write("./dirB/showE/title S03E06.e")
|
||||
|
||||
// defaults
|
||||
write("./dirA/showF/.show-ingestion.yaml", `{
|
||||
"o": "`+outd+`/F"
|
||||
}`)
|
||||
write("./dirA/showF/[Yameii] Dr. Stone - S04E12 [English Dub] [CR WEB-DL 720p] [F6EF1948].mkv")
|
||||
|
||||
if err := main.Recursive(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := main.Recursive(context.Background()); err != nil {
|
||||
t.Fatalf("failed second run: %v", err)
|
||||
}
|
||||
|
||||
exists(t, path.Join(outd, "A", "A_SAEA.a"))
|
||||
exists(t, path.Join(outd, "B_TITLE_01_02", "TITLE_S01E02.b"))
|
||||
exists(t, path.Join(outd, "C", "t_SsEe.c"))
|
||||
notExists(t, path.Join(outd, "D", "title_S02E04.d"))
|
||||
notExists(t, path.Join(outd, "title_S03E06.e"))
|
||||
exists(t, path.Join(outd, "F", "Dr_Stone_S04E12.mkv"))
|
||||
notExists(t, path.Join(outd, "F", "[Yameii]_Dr_Stone_-_S04E12.mkv"))
|
||||
}
|
||||
|
||||
func write(f string, b ...string) {
|
||||
if len(b) == 0 {
|
||||
b = append(b, "")
|
||||
}
|
||||
|
||||
os.MkdirAll(path.Dir(f), os.ModePerm)
|
||||
os.WriteFile(f, []byte(b[0]), os.ModePerm)
|
||||
}
|
||||
|
||||
func exists(t *testing.T, p string) {
|
||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||
d := path.Dir(path.Dir(p))
|
||||
t.Errorf("expected %s of %s (%+v)", path.Base(p), d, ls(d))
|
||||
}
|
||||
}
|
||||
|
||||
func notExists(t *testing.T, p string) {
|
||||
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
||||
d := path.Dir(path.Dir(p))
|
||||
t.Errorf("unexpected %s of %s (%+v)", path.Base(p), d, ls(d))
|
||||
}
|
||||
}
|
||||
|
||||
func ls(d string) []string {
|
||||
result := []string{}
|
||||
entries, _ := os.ReadDir(d)
|
||||
for _, entry := range entries {
|
||||
p := path.Join(d, entry.Name())
|
||||
if entry.IsDir() {
|
||||
result = append(result, ls(p)...)
|
||||
} else {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
slices.Sort(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
pattern string
|
||||
want main.Fields
|
||||
}{
|
||||
"[SubsPlease] Tokidoki Bosotto Russia-go de Dereru Tonari no Alya-san - 01 (720p) [A12844D5].mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "Tokidoki Bosotto Russia-go de Dereru Tonari no Alya-san",
|
||||
Season: "",
|
||||
Episode: "01",
|
||||
},
|
||||
},
|
||||
"Survivor.AU.S12E11.1080p.HEVC.x265-MeGusta[EZTVx.to].mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "Survivor.AU",
|
||||
Season: "12",
|
||||
Episode: "11",
|
||||
},
|
||||
},
|
||||
"DAN DA DAN (2024) S01E01v2 (1080p WEB-DL H264 AAC DDP 2.0 Dual-Audio) [MALD].mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "DAN DA DAN (2024)",
|
||||
Season: "01",
|
||||
Episode: "01",
|
||||
},
|
||||
},
|
||||
"ZENSHU.S01E01.1080p.AMZN.WEB-DL.MULTi.DDP2.0.H.264.MSubs-ToonsHub.mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "ZENSHU",
|
||||
Season: "01",
|
||||
Episode: "01",
|
||||
},
|
||||
},
|
||||
"[Yameii] My Hero Academia - S07E08 [English Dub] [CR WEB-DL 720p] [DE5FFC3E].mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "My Hero Academia",
|
||||
Season: "07",
|
||||
Episode: "08",
|
||||
},
|
||||
},
|
||||
"Ranma1-2.2024.S01E03.Because.Theres.Someone.He.Likes.1080p.NF.WEB-DL.AAC2.0.H.264-VARYG.mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "Ranma1-2.2024",
|
||||
Season: "01",
|
||||
Episode: "03",
|
||||
},
|
||||
},
|
||||
"[Yameii] The Apothecary Diaries - S02E03 [English Dub] [CR WEB-DL 720p] [FD3E7434].mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "The Apothecary Diaries",
|
||||
Season: "02",
|
||||
Episode: "03",
|
||||
},
|
||||
},
|
||||
"The.Dinner.Table.Detective.S01E01.Welcome.to.the.murderous.party.File.1.1080p.AMZN.WEB-DL.DDP2.0.H.264-VARYG.mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "The.Dinner.Table.Detective",
|
||||
Season: "01",
|
||||
Episode: "01",
|
||||
},
|
||||
},
|
||||
"[Reza] Wistoria Wand and Sword - S01E01.mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "Wistoria Wand and Sword",
|
||||
Season: "01",
|
||||
Episode: "01",
|
||||
},
|
||||
},
|
||||
"[EMBER] Ao no Hako - 01.mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "Ao no Hako",
|
||||
Season: "",
|
||||
Episode: "01",
|
||||
},
|
||||
},
|
||||
"Niehime to Kemono no Ou - 12 [darkflux].mkv": {
|
||||
pattern: main.PatternGroupTitleHyphenSE,
|
||||
want: main.Fields{
|
||||
Title: "Niehime to Kemono no Ou",
|
||||
Season: "",
|
||||
Episode: "12",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for f, d := range cases {
|
||||
c := d
|
||||
t.Run(f, func(t *testing.T) {
|
||||
got, _ := main.Parse(f, c.pattern)
|
||||
if got != c.want {
|
||||
t.Errorf("expected \n\t%+v but got \n\t%+v", c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user