package categorize import ( "errors" "regexp" "strconv" "strings" "unicode/utf8" "github.com/ollien/gobbler/categorize/match" ) const seasonPattern = `(?i)S(?:eason(?P.*?))?(?P\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 }