mirror of https://github.com/stashapp/stash.git
199 lines
4.3 KiB
Go
199 lines
4.3 KiB
Go
|
// Package autotag provides methods to auto-tag scenes with performers,
|
||
|
// studios and tags.
|
||
|
//
|
||
|
// The autotag engine tags scenes with performers/studios/tags if the scene's
|
||
|
// path matches the performer/studio/tag name. A scene's path is considered
|
||
|
// a match if it contains the performer/studio/tag's full name, ignoring any
|
||
|
// '.', '-', '_' characters in the path.
|
||
|
//
|
||
|
// For example, for a performer "foo bar", the following paths would be
|
||
|
// considered a match: "foo bar.mp4", "foobar.mp4", "foo.bar.mp4",
|
||
|
// "foo-bar.mp4", "aaa.foo bar.bbb.mp4".
|
||
|
// The following would not be considered a match:
|
||
|
// "aafoo bar.mp4", "foo barbb.mp4", "foo/bar.mp4"
|
||
|
package autotag
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/stashapp/stash/pkg/logger"
|
||
|
"github.com/stashapp/stash/pkg/models"
|
||
|
)
|
||
|
|
||
|
const separatorChars = `.\-_ `
|
||
|
|
||
|
// fixes #1292
|
||
|
func escapePathRegex(name string) string {
|
||
|
ret := name
|
||
|
|
||
|
chars := `+*?()|[]{}^$`
|
||
|
for _, c := range chars {
|
||
|
cStr := string(c)
|
||
|
ret = strings.ReplaceAll(ret, cStr, `\`+cStr)
|
||
|
}
|
||
|
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
func getPathQueryRegex(name string) string {
|
||
|
// escape specific regex characters
|
||
|
name = escapePathRegex(name)
|
||
|
|
||
|
// handle path separators
|
||
|
const separator = `[` + separatorChars + `]`
|
||
|
|
||
|
ret := strings.Replace(name, " ", separator+"*", -1)
|
||
|
ret = `(?:^|_|[^\w\d])` + ret + `(?:$|_|[^\w\d])`
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
func nameMatchesPath(name, path string) bool {
|
||
|
// escape specific regex characters
|
||
|
name = escapePathRegex(name)
|
||
|
|
||
|
name = strings.ToLower(name)
|
||
|
path = strings.ToLower(path)
|
||
|
|
||
|
// handle path separators
|
||
|
const separator = `[` + separatorChars + `]`
|
||
|
|
||
|
reStr := strings.Replace(name, " ", separator+"*", -1)
|
||
|
reStr = `(?:^|_|[^\w\d])` + reStr + `(?:$|_|[^\w\d])`
|
||
|
|
||
|
re := regexp.MustCompile(reStr)
|
||
|
return re.MatchString(path)
|
||
|
}
|
||
|
|
||
|
func getPathWords(path string) []string {
|
||
|
retStr := path
|
||
|
|
||
|
// remove the extension
|
||
|
ext := filepath.Ext(retStr)
|
||
|
if ext != "" {
|
||
|
retStr = strings.TrimSuffix(retStr, ext)
|
||
|
}
|
||
|
|
||
|
// handle path separators
|
||
|
const separator = `(?:_|[^\w\d])+`
|
||
|
re := regexp.MustCompile(separator)
|
||
|
retStr = re.ReplaceAllString(retStr, " ")
|
||
|
|
||
|
words := strings.Split(retStr, " ")
|
||
|
|
||
|
// remove any single letter words
|
||
|
var ret []string
|
||
|
for _, w := range words {
|
||
|
if len(w) > 1 {
|
||
|
ret = append(ret, w)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
type tagger struct {
|
||
|
ID int
|
||
|
Type string
|
||
|
Name string
|
||
|
Path string
|
||
|
}
|
||
|
|
||
|
type addLinkFunc func(subjectID, otherID int) (bool, error)
|
||
|
|
||
|
func (t *tagger) addError(otherType, otherName string, err error) error {
|
||
|
return fmt.Errorf("error adding %s '%s' to %s '%s': %s", otherType, otherName, t.Type, t.Name, err.Error())
|
||
|
}
|
||
|
|
||
|
func (t *tagger) addLog(otherType, otherName string) {
|
||
|
logger.Infof("Added %s '%s' to %s '%s'", otherType, otherName, t.Type, t.Name)
|
||
|
}
|
||
|
|
||
|
func (t *tagger) tagPerformers(performerReader models.PerformerReader, addFunc addLinkFunc) error {
|
||
|
others, err := getMatchingPerformers(t.Path, performerReader)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, p := range others {
|
||
|
added, err := addFunc(t.ID, p.ID)
|
||
|
|
||
|
if err != nil {
|
||
|
return t.addError("performer", p.Name.String, err)
|
||
|
}
|
||
|
|
||
|
if added {
|
||
|
t.addLog("performer", p.Name.String)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *tagger) tagStudios(studioReader models.StudioReader, addFunc addLinkFunc) error {
|
||
|
others, err := getMatchingStudios(t.Path, studioReader)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// only add first studio
|
||
|
if len(others) > 0 {
|
||
|
studio := others[0]
|
||
|
added, err := addFunc(t.ID, studio.ID)
|
||
|
|
||
|
if err != nil {
|
||
|
return t.addError("studio", studio.Name.String, err)
|
||
|
}
|
||
|
|
||
|
if added {
|
||
|
t.addLog("studio", studio.Name.String)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *tagger) tagTags(tagReader models.TagReader, addFunc addLinkFunc) error {
|
||
|
others, err := getMatchingTags(t.Path, tagReader)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, p := range others {
|
||
|
added, err := addFunc(t.ID, p.ID)
|
||
|
|
||
|
if err != nil {
|
||
|
return t.addError("tag", p.Name, err)
|
||
|
}
|
||
|
|
||
|
if added {
|
||
|
t.addLog("tag", p.Name)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (t *tagger) tagScenes(paths []string, sceneReader models.SceneReader, addFunc addLinkFunc) error {
|
||
|
others, err := getMatchingScenes(t.Name, paths, sceneReader)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, p := range others {
|
||
|
added, err := addFunc(t.ID, p.ID)
|
||
|
|
||
|
if err != nil {
|
||
|
return t.addError("scene", p.GetTitle(), err)
|
||
|
}
|
||
|
|
||
|
if added {
|
||
|
t.addLog("scene", p.GetTitle())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|