From 4318414c02363e14f755b0003bfda83ee80c32ec Mon Sep 17 00:00:00 2001 From: Martin Polden Date: Mon, 20 Sep 2021 19:19:52 +0200 Subject: cmd: Respect replacements when classifying --- cmd/lftpq/main.go | 53 ++++++++++++++++++++++++++++---------------------- cmd/lftpq/main_test.go | 17 +++++++++++++--- parser/parser.go | 12 ------------ queue/config.go | 32 +++++++++++++++++------------- queue/config_test.go | 6 +++--- queue/item.go | 34 +++++++++++++------------------- queue/item_test.go | 34 ++++++++++++++++---------------- queue/queue.go | 4 ++-- queue/queue_test.go | 26 ++++++++++++------------- 9 files changed, 110 insertions(+), 108 deletions(-) diff --git a/cmd/lftpq/main.go b/cmd/lftpq/main.go index 65837d7..6b752bf 100644 --- a/cmd/lftpq/main.go +++ b/cmd/lftpq/main.go @@ -7,10 +7,10 @@ import ( "os" "os/signal" "path/filepath" + "sort" "syscall" "github.com/mpolden/lftpq/lftp" - "github.com/mpolden/lftpq/parser" "github.com/mpolden/lftpq/queue" ) @@ -66,28 +66,7 @@ func (c *CLI) Run() error { return nil } if c.Name != "" { - name := filepath.Base(c.Name) - media, parserName, err := parser.Guess(name) - if err != nil { - return err - } - templateFound := false - for _, dir := range cfg.LocalDirs { - if dir.Parser != parserName { - continue - } - path, err := media.PathIn(dir.Template) - if err != nil { - return err - } - templateFound = true - fmt.Fprintln(c.stdout, path) - break - } - if !templateFound { - return fmt.Errorf("no template set for parser: %s", parserName) - } - return nil + return c.classify(cfg.LocalDirs) } var queues []queue.Queue if c.Import { @@ -110,6 +89,34 @@ func (c *CLI) Run() error { return nil } +func (c *CLI) classify(dirs []queue.LocalDir) error { + name := filepath.Base(c.Name) + sortedDirs := make([]queue.LocalDir, len(dirs)) + copy(sortedDirs, dirs) + // Always try show parsers first + sort.Slice(sortedDirs, func(i, j int) bool { + return sortedDirs[i].Parser == "show" && sortedDirs[j].Parser != "show" + }) + parsed := false + for _, dir := range sortedDirs { + media, err := dir.Media(name) + if err != nil { + return err + } + path, err := media.PathIn(dir.Template) + if err != nil { + return err + } + parsed = true + fmt.Fprintln(c.stdout, path) + break + } + if !parsed { + return fmt.Errorf("parsing failed: %q", name) + } + return nil +} + func (c *CLI) lockfile() string { return filepath.Join(os.TempDir(), ".lftpqlock") } func (c *CLI) lock() error { diff --git a/cmd/lftpq/main_test.go b/cmd/lftpq/main_test.go index b2de28e..bfe6910 100644 --- a/cmd/lftpq/main_test.go +++ b/cmd/lftpq/main_test.go @@ -121,7 +121,7 @@ func TestConfigTest(t *testing.T) { "Name": "d1", "Parser": "movie", "Dir": "/tmp/", - "Replacements": null + "Replacements": [] } ], "Sites": [] @@ -417,17 +417,28 @@ func TestClassify(t *testing.T) { "Name": "d1", "Parser": "movie", "Dir": "/media/{{ .Year}}/" + }, + { + "Name": "d2", + "Parser": "show", + "Dir": "/media/{{ .Name }}/S{{ .Season | Sprintf \"%02d\" }}/", + "Replacements": [ + { + "Pattern": "foo", + "Replacement": "Foo" + } + ] } ] }`) defer os.Remove(cli.Config) - cli.Name = "/download/foo.2018" + cli.Name = "/download/foo.S01E01" if err := cli.Run(); err != nil { t.Fatal(err) } - want := "/media/2018/foo.2018\n" + want := "/media/Foo/S01/foo.S01E01\n" if got := buf.String(); got != want { t.Errorf("want %q, got %q", want, got) } diff --git a/parser/parser.go b/parser/parser.go index 3378154..621992d 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -60,18 +60,6 @@ func (m *Media) PathIn(dir *template.Template) (string, error) { return path, nil } -func Guess(s string) (Media, string, error) { - show, err := Show(s) - if err != nil { - movie, err := Movie(s) - if err != nil { - return Media{}, "", fmt.Errorf("could not guess media: %q", s) - } - return movie, "movie", err - } - return show, "show", nil -} - func Default(s string) (Media, error) { return Media{Release: s}, nil } diff --git a/queue/config.go b/queue/config.go index bff5572..59b07a7 100644 --- a/queue/config.go +++ b/queue/config.go @@ -35,6 +35,7 @@ type LocalDir struct { Dir string Replacements []Replacement Template *template.Template `json:"-"` + parser parser.Parser } type Site struct { @@ -51,19 +52,24 @@ type Site struct { SkipExisting bool SkipFiles bool LocalDir string + localDir LocalDir Priorities []string priorities []*regexp.Regexp PostCommand string postCommand *exec.Cmd Merge bool Skip bool - itemParser } -type itemParser struct { - parser parser.Parser - template *template.Template - replacements []Replacement +func (d *LocalDir) Media(name string) (parser.Media, error) { + m, err := d.parser(filepath.Base(name)) + if err != nil { + return parser.Media{}, err + } + for _, r := range d.Replacements { + m.ReplaceName(r.pattern, r.Replacement) + } + return m, nil } func compilePatterns(patterns []string) ([]*regexp.Regexp, error) { @@ -129,7 +135,7 @@ func command(cmd string) (*exec.Cmd, error) { } func (c *Config) load() error { - itemParsers := make(map[string]itemParser) + localDirs := make(map[string]LocalDir) for i, d := range c.LocalDirs { if d.Name == "" { return fmt.Errorf("invalid local dir name: %q", d.Name) @@ -137,7 +143,7 @@ func (c *Config) load() error { if d.Dir == "" { return fmt.Errorf("invalid local dir path: %q", d.Dir) } - if _, ok := itemParsers[d.Name]; ok { + if _, ok := localDirs[d.Name]; ok { return fmt.Errorf("invalid local dir: %q: declared multiple times", d.Name) } var parserFunc parser.Parser @@ -160,12 +166,10 @@ func (c *Config) load() error { if err != nil { return err } - itemParsers[d.Name] = itemParser{ - parser: parserFunc, - replacements: replacements, - template: tmpl, - } + c.LocalDirs[i].parser = parserFunc + c.LocalDirs[i].Replacements = replacements c.LocalDirs[i].Template = tmpl + localDirs[d.Name] = c.LocalDirs[i] } for i := range c.Sites { site := &c.Sites[i] @@ -196,11 +200,11 @@ func (c *Config) load() error { } site.postCommand = cmd - itemParser, ok := itemParsers[site.LocalDir] + localDir, ok := localDirs[site.LocalDir] if !ok { return fmt.Errorf("site: %q: invalid local dir: %q", site.Name, site.LocalDir) } - site.itemParser = itemParser + site.localDir = localDir } return nil } diff --git a/queue/config_test.go b/queue/config_test.go index 7cf57a2..f5c19cc 100644 --- a/queue/config_test.go +++ b/queue/config_test.go @@ -52,13 +52,13 @@ func TestLoad(t *testing.T) { if len(site.priorities) == 0 { t.Error("Expected non-empty priorities") } - if site.itemParser.template == nil { + if site.localDir.Template == nil { t.Error("Expected template to be compiled") } - if site.itemParser.parser == nil { + if site.localDir.parser == nil { t.Error("Expected parser to be set") } - if len(site.itemParser.replacements) == 0 { + if len(site.localDir.Replacements) == 0 { t.Error("Expected non-empty replacements") } if want := []string{"xargs", "echo"}; !reflect.DeepEqual(want, site.postCommand.Args) { diff --git a/queue/item.go b/queue/item.go index 27d3821..7ea6e78 100644 --- a/queue/item.go +++ b/queue/item.go @@ -16,7 +16,7 @@ type Item struct { Media parser.Media Duplicate bool Merged bool - itemParser + localDir LocalDir } func (i *Item) isEmpty(readDir readDir) bool { @@ -34,18 +34,6 @@ func (i *Item) reject(reason string) { i.Reason = reason } -func (i *Item) setMedia(dirname string) error { - m, err := i.itemParser.parser(dirname) - if err != nil { - return err - } - for _, r := range i.itemParser.replacements { - m.ReplaceName(r.pattern, r.Replacement) - } - i.Media = m - return nil -} - func (i *Item) duplicates(readDir readDir) []Item { var items []Item parent := filepath.Join(i.LocalPath, "..") @@ -56,7 +44,7 @@ func (i *Item) duplicates(readDir readDir) []Item { continue } path := filepath.Join(parent, fi.Name()) - item, err := newItem(path, i.ModTime, i.itemParser) + item, err := newItem(path, i.ModTime, i.localDir) if err != nil { item.reject(err.Error()) } else { @@ -72,12 +60,16 @@ func (i *Item) duplicates(readDir readDir) []Item { return items } -func newItem(remotePath string, modTime time.Time, itemParser itemParser) (Item, error) { - item := Item{RemotePath: remotePath, ModTime: modTime, Reason: "no match", itemParser: itemParser} - if err := item.setMedia(filepath.Base(remotePath)); err != nil { - return item, err +func newItem(remotePath string, modTime time.Time, localDir LocalDir) (Item, error) { + item := Item{RemotePath: remotePath, ModTime: modTime, Reason: "no match", localDir: localDir} + media, err := localDir.Media(remotePath) + if err != nil { + return Item{}, err + } + item.Media = media + item.LocalPath, err = media.PathIn(localDir.Template) + if err != nil { + return Item{}, err } - var err error - item.LocalPath, err = item.Media.PathIn(itemParser.template) - return item, err + return item, nil } diff --git a/queue/item_test.go b/queue/item_test.go index 8e6375b..b7b2d03 100644 --- a/queue/item_test.go +++ b/queue/item_test.go @@ -10,41 +10,41 @@ import ( "github.com/mpolden/lftpq/parser" ) -func newTestItem(remotePath string, itemParser itemParser) Item { - item, _ := newItem(remotePath, time.Time{}, itemParser) +func newTestItem(remotePath string, localDir LocalDir) Item { + item, _ := newItem(remotePath, time.Time{}, localDir) return item } -func showItemParser() itemParser { - return itemParser{ +func showDir() LocalDir { + return LocalDir{ parser: parser.Show, - template: template.Must(template.New("t").Parse("/tmp/{{ .Name }}/S{{ .Season }}/")), + Template: template.Must(template.New("t").Parse("/tmp/{{ .Name }}/S{{ .Season }}/")), } } -func movieItemParser() itemParser { - return itemParser{ +func movieDir() LocalDir { + return LocalDir{ parser: parser.Movie, - template: template.Must(template.New("t").Parse("/tmp/{{ .Year }}/{{ .Name }}/")), + Template: template.Must(template.New("t").Parse("/tmp/{{ .Year }}/{{ .Name }}/")), } } func TestNewItemShow(t *testing.T) { - item := newTestItem("/foo/The.Wire.S03E01", showItemParser()) + item := newTestItem("/foo/The.Wire.S03E01", showDir()) if expected := "/tmp/The.Wire/S3/The.Wire.S03E01"; item.LocalPath != expected { t.Fatalf("Expected %q, got %q", expected, item.LocalPath) } } func TestNewItemMovie(t *testing.T) { - item := newTestItem("/foo/Apocalypse.Now.1979", movieItemParser()) + item := newTestItem("/foo/Apocalypse.Now.1979", movieDir()) if expected := "/tmp/1979/Apocalypse.Now/Apocalypse.Now.1979"; item.LocalPath != expected { t.Fatalf("Expected %q, got %q", expected, item.LocalPath) } } func TestNewItemDefaultParser(t *testing.T) { - tmpl := itemParser{parser: parser.Default, template: template.Must(template.New("t").Parse("/tmp/"))} + tmpl := LocalDir{parser: parser.Default, Template: template.Must(template.New("t").Parse("/tmp/"))} item := newTestItem("/foo/The.Wire.S03E01", tmpl) if expected := "/tmp/The.Wire.S03E01"; item.LocalPath != expected { t.Fatalf("Expected %s, got %s", expected, item.LocalPath) @@ -52,15 +52,15 @@ func TestNewItemDefaultParser(t *testing.T) { } func TestNewItemUnparsable(t *testing.T) { - _, err := newItem("/foo/bar", time.Time{}, showItemParser()) + _, err := newItem("/foo/bar", time.Time{}, showDir()) if err == nil { t.Fatal("Expected error") } } func TestNewItemWithReplacements(t *testing.T) { - tmpl := showItemParser() - tmpl.replacements = []Replacement{ + tmpl := showDir() + tmpl.Replacements = []Replacement{ {pattern: regexp.MustCompile("_"), Replacement: "."}, {pattern: regexp.MustCompile(`\.Of\.`), Replacement: ".of."}, {pattern: regexp.MustCompile(`\.the\.`), Replacement: ".The."}, @@ -92,11 +92,11 @@ func TestLocalPath(t *testing.T) { {"/remote/bar", "/local/bar", "/local/bar"}, } for _, tt := range tests { - itemParser := itemParser{ + localDir := LocalDir{ parser: parser.Default, - template: template.Must(template.New("t").Parse(tt.template)), + Template: template.Must(template.New("t").Parse(tt.template)), } - item := newTestItem(tt.remotePath, itemParser) + item := newTestItem(tt.remotePath, localDir) if item.LocalPath != tt.out { t.Errorf("Expected %q, got %q", tt.out, item.LocalPath) } diff --git a/queue/queue.go b/queue/queue.go index daa3b71..d2340d2 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -63,7 +63,7 @@ func Read(sites []Site, r io.Reader) ([]Queue, error) { indices[site.Name] = i } q := &qs[i] - item, err := newItem(fields[1], time.Time{}, q.itemParser) + item, err := newItem(fields[1], time.Time{}, q.localDir) if err != nil { item.reject(err.Error()) } else { @@ -216,7 +216,7 @@ func newQueue(site Site, files []os.FileInfo, readDir readDir) Queue { // Initial filtering now := time.Now().Round(time.Second) for _, f := range files { - item, err := newItem(f.Name(), f.ModTime(), q.itemParser) + item, err := newItem(f.Name(), f.ModTime(), q.localDir) if err != nil { item.reject(err.Error()) } else if isSymlink := f.Mode()&os.ModeSymlink != 0; q.SkipSymlinks && isSymlink { diff --git a/queue/queue_test.go b/queue/queue_test.go index e40222d..b98237c 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -14,14 +14,14 @@ import ( ) func newTestSite() Site { - localDir := template.Must(template.New("t").Parse("/local/{{ .Name }}/S{{ .Season }}/")) + tmpl := template.Must(template.New("t").Parse("/local/{{ .Name }}/S{{ .Season }}/")) patterns := []*regexp.Regexp{regexp.MustCompile(".*")} return Site{ GetCmd: "mirror", Name: "test", patterns: patterns, - itemParser: itemParser{ - template: localDir, + localDir: LocalDir{ + Template: tmpl, parser: parser.Show, }, } @@ -55,8 +55,8 @@ func TestNewQueue(t *testing.T) { SkipSymlinks: true, SkipExisting: true, SkipFiles: true, - itemParser: itemParser{ - template: template.Must(template.New("localDir").Parse("/local/")), + localDir: LocalDir{ + Template: template.Must(template.New("localDir").Parse("/local/")), parser: parser.Default, }, } @@ -117,14 +117,14 @@ func TestNewQueue(t *testing.T) { } q := newQueue(s, files, readDir) expected := []Item{ - {itemParser: q.itemParser, RemotePath: files[0].Name(), Transfer: false, Reason: "IsSymlink=true SkipSymlinks=true"}, - {itemParser: q.itemParser, RemotePath: files[1].Name(), Transfer: false, Reason: "Age=48h0m0s MaxAge=24h0m0s"}, - {itemParser: q.itemParser, RemotePath: files[2].Name(), Transfer: true, Reason: "Match=dir\\d"}, - {itemParser: q.itemParser, RemotePath: files[3].Name(), Transfer: true, Reason: "Match=dir\\d"}, - {itemParser: q.itemParser, RemotePath: files[4].Name(), Transfer: false, Reason: "IsDstDirEmpty=false"}, - {itemParser: q.itemParser, RemotePath: files[5].Name(), Transfer: false, Reason: "no match"}, - {itemParser: q.itemParser, RemotePath: files[6].Name(), Transfer: false, Reason: "Filter=^incomplete-"}, - {itemParser: q.itemParser, RemotePath: files[7].Name(), Transfer: false, Reason: "IsFile=true SkipFiles=true"}, + {localDir: q.localDir, RemotePath: files[0].Name(), Transfer: false, Reason: "IsSymlink=true SkipSymlinks=true"}, + {localDir: q.localDir, RemotePath: files[1].Name(), Transfer: false, Reason: "Age=48h0m0s MaxAge=24h0m0s"}, + {localDir: q.localDir, RemotePath: files[2].Name(), Transfer: true, Reason: "Match=dir\\d"}, + {localDir: q.localDir, RemotePath: files[3].Name(), Transfer: true, Reason: "Match=dir\\d"}, + {localDir: q.localDir, RemotePath: files[4].Name(), Transfer: false, Reason: "IsDstDirEmpty=false"}, + {localDir: q.localDir, RemotePath: files[5].Name(), Transfer: false, Reason: "no match"}, + {localDir: q.localDir, RemotePath: files[6].Name(), Transfer: false, Reason: "Filter=^incomplete-"}, + {localDir: q.localDir, RemotePath: files[7].Name(), Transfer: false, Reason: "IsFile=true SkipFiles=true"}, } actual := q.Items if len(expected) != len(actual) { -- cgit v1.2.3