gobbler/categorize/tv.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
}