103 lines
2.8 KiB
Go
103 lines
2.8 KiB
Go
package categorize
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/ollien/gobbler/categorize/match"
|
|
)
|
|
|
|
const seasonPattern = `(?i)S(?:eason(?P<sep>.*?))?(?P<seasonNumber>\d+)`
|
|
|
|
var errNotTvShow = errors.New("no tv-like information found in show name")
|
|
var errNoSeasonInfo = errors.New("could not find season info")
|
|
|
|
func getSeasonForFile(filename string) (int, error) {
|
|
rawSeasonNumber, err := extractSeasonNumberString(filename)
|
|
if err != nil {
|
|
return 0, errNoSeasonInfo
|
|
}
|
|
|
|
seasonNumber, err := strconv.Atoi(rawSeasonNumber)
|
|
if err != nil {
|
|
// We should panic here because this can truly _NEVER_ happen, and is not recoverable.
|
|
// The regular expression should return a number and anything else is programmer error
|
|
panic(err)
|
|
}
|
|
|
|
return seasonNumber, nil
|
|
}
|
|
|
|
func findShowFolder(filename string, showFolders []string) (string, error) {
|
|
possibleShowName, err := extractShowName(filename)
|
|
if err != nil {
|
|
return "", errNotTvShow
|
|
}
|
|
|
|
return matchToFolder(possibleShowName, showFolders)
|
|
}
|
|
|
|
func extractShowName(filename string) (string, error) {
|
|
seasonRegex := regexp.MustCompile(seasonPattern)
|
|
seasonLocation := seasonRegex.FindStringIndex(filename)
|
|
if seasonLocation == nil {
|
|
return "", errors.New("no season information found")
|
|
}
|
|
|
|
seasonStartPos := seasonLocation[0]
|
|
possibleShowName := filename[:seasonStartPos]
|
|
|
|
return strings.TrimFunc(possibleShowName, isTrimmableSymbol), nil
|
|
}
|
|
|
|
func matchToFolder(candidate string, showFolders []string) (string, error) {
|
|
return match.FindBestMatch(candidate, showFolders)
|
|
}
|
|
|
|
func extractSeasonNumberString(filename string) (string, error) {
|
|
seasonRegex := regexp.MustCompile(seasonPattern)
|
|
match := seasonRegex.FindStringSubmatch(filename)
|
|
if len(match) == 0 {
|
|
return "", errNoSeasonInfo
|
|
}
|
|
|
|
namedGroups := matchIndexToGroupNames(seasonRegex, match)
|
|
// We don't want to match S(some garbage)number, unless it's truly only the same separator repeated.
|
|
// Regex is not powerful enough to check for this on its own so we must do it manually
|
|
if !onlyConsistsOfOneChar(namedGroups["sep"]) {
|
|
return "", errNoSeasonInfo
|
|
}
|
|
|
|
return namedGroups["seasonNumber"], nil
|
|
}
|
|
|
|
// matchIndexToGroupNames converts the result form FindStringSubmatch to a map of the named groups
|
|
func matchIndexToGroupNames(pattern *regexp.Regexp, groups []string) map[string]string {
|
|
res := map[string]string{}
|
|
for i, name := range pattern.SubexpNames() {
|
|
res[name] = groups[i]
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// onlyConsistsOfOneChar checks if a given string consists of only one char, e.g. "aaaa" returns true, but "abba"
|
|
// returns false. The empty string, despite having no chars, is defined to meet this condition.
|
|
func onlyConsistsOfOneChar(s string) bool {
|
|
if len(s) == 0 {
|
|
return true
|
|
}
|
|
|
|
firstRune, _ := utf8.DecodeRuneInString(s)
|
|
for _, c := range s[1:] {
|
|
if c != firstRune {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|