func getFilename(tc *tivo.Tivo, maze *tvmaze.Client, detail *tivo.VideoDetail) (string, error) { filename := detail.Title if detail.IsEpisodic { show, err := maze.FindShow(detail.Title) if err != nil { return "", err } episodes, err := maze.GetEpisodes(show.ID) if err != nil { return "", err } episode, err := findEpisode(detail, episodes) if err != nil { fmt.Println(err.Error()) // Some shows have several candidates in the tvrage api, and no data // in the tivo to disambiguate the candidates (Being Human for example). // If the episode_number is all digits, it's hopefully accurate. re := regexp.MustCompile(`^(\d{1,})(\d{2})$`) captures := re.FindStringSubmatch(detail.EpisodeNumber) if captures != nil { s, serr := strconv.Atoi(captures[1]) ep, eperr := strconv.Atoi(captures[2]) if serr == nil && eperr == nil { filename += fmt.Sprintf(" %dx%.2d-%s", s, ep, detail.EpisodeTitle) } else if serr != nil { return "", errors.New("string to int conversion failed for serr: " + serr.Error()) } else if serr != nil { return "", errors.New("string to int conversion failed for eperr: " + eperr.Error()) } } } else { detail.EpisodeNumber = fmt.Sprintf("%d%.2d", episode.Season, episode.Number) filename += fmt.Sprintf(" %dx%.2d-%s", episode.Season, episode.Number, detail.EpisodeTitle) } } filename = strings.Replace(filename, "/", "-", -1) return filename, nil }
func findEpisode(detail *tivo.VideoDetail, episodes []tvmaze.Episode) (tvmaze.Episode, error) { for desperate := 0; desperate <= 3; desperate++ { for _, episode := range episodes { // normalize chars ’ vs ' etc. var mtb []byte var ttb []byte var tivotitle string var mazetitle string if len(episode.Name) == len(detail.EpisodeTitle) { mtb = []byte(episode.Name) ttb = []byte(detail.EpisodeTitle) for i := 0; i < len(mtb); i++ { ord := int(mtb[i]) if ord < 32 || ord > 126 { mtb = append(mtb[:i], mtb[i+1:]...) ttb = append(ttb[:i], ttb[i+1:]...) } } tivotitle = string(ttb[:]) mazetitle = string(mtb[:]) } else { tivotitle = detail.EpisodeTitle mazetitle = episode.Name } // As we become more desperate to find a match strip out non-word characters // to make a match more likely. if desperate >= 2 { re := regexp.MustCompile(`\W`) tivotitle = string(re.ReplaceAll([]byte(tivotitle), []byte(""))) mazetitle = string(re.ReplaceAll([]byte(mazetitle), []byte(""))) } // exact title match if strings.ToLower(mazetitle) == strings.ToLower(tivotitle) { return episode, nil // exact title match if you add part_index inside parens to tivo title } else if detail.PartIndex > 0 && desperate == 0 { tt := fmt.Sprintf("%s (%d)", tivotitle, detail.PartIndex) if tt == mazetitle { return episode, nil } // rage title contains tivo title } else if desperate == 1 && strings.Contains(mazetitle, tivotitle) { return episode, nil // tivo title contains rage title } else if desperate == 1 && strings.Contains(tivotitle, mazetitle) { detail.EpisodeNumber = fmt.Sprintf("%d%.2d", episode.Season, episode.Number) return episode, nil } else if desperate == 1 { // try to match 'Kill Billie: Vol.2' with 'Kill Billie (2)' re := regexp.MustCompile(`\((\d+)\)/`) captures := re.FindStringSubmatch(mazetitle) if captures != nil { mt := string(re.ReplaceAll([]byte(mazetitle), []byte(""))) sequel := captures[1] mt = strings.TrimSpace(mt) if strings.Contains(tivotitle, mt) && strings.Contains(tivotitle, sequel) { return episode, nil } } else if strings.Contains(mazetitle, " and ") && strings.Contains(tivotitle, "&") { tt := strings.Replace(tivotitle, "&", "and", -1) if mazetitle == tt { return episode, nil } } else if strings.Contains(mazetitle, "&") && strings.Contains(tivotitle, " and ") { tt := strings.Replace(tivotitle, " and ", " & ", -1) if mazetitle == tt { return episode, nil } } } } } return tvmaze.Episode{}, errors.New("Failed to ID season and episode!") }