package cmd import ( "fmt" "os" "strconv" "strings" "gitea.dcglab.co.uk/steve/arrman/sonarr" ) func filterSeries(series []sonarr.Series, term string) []sonarr.Series { lower := strings.ToLower(term) var filtered []sonarr.Series for _, s := range series { if strings.Contains(strings.ToLower(s.Title), lower) { filtered = append(filtered, s) } } return filtered } type SonarrClient interface { List() ([]sonarr.Series, error) Lookup(term string) ([]sonarr.Series, error) Get(id int) (*sonarr.Series, error) QualityProfiles() ([]sonarr.QualityProfile, error) RootFolders() ([]sonarr.RootFolder, error) Add(s sonarr.Series) (*sonarr.Series, error) } func TVList(client SonarrClient, term string, includeExternal bool, jsonOut bool) error { var series []sonarr.Series var err error if term != "" && includeExternal { series, err = client.Lookup(term) } else { series, err = client.List() if err == nil && term != "" { series = filterSeries(series, term) } } if err != nil { return err } if jsonOut { out := make([]jsonSeries, len(series)) for i, s := range series { out[i] = seriesToJSON(s) } return writeJSON(out) } if len(series) == 0 { fmt.Println("No series found") return nil } for _, s := range series { eps := "" if s.Statistics != nil { pct := 0.0 if s.Statistics.EpisodeCount > 0 { pct = float64(s.Statistics.EpisodeFileCount) / float64(s.Statistics.EpisodeCount) * 100 } eps = fmt.Sprintf(" — Episodes: %d/%d (%.0f%%)", s.Statistics.EpisodeFileCount, s.Statistics.EpisodeCount, pct) } fmt.Printf("%s (%d) — %s%s\n", s.Title, s.Year, s.Status, eps) } return nil } func TVAdd(client SonarrClient, args []string, jsonOut bool) error { term, profile, rootfolder, err := parseAddArgs(args) if err != nil { return err } profileID, err := strconv.Atoi(profile) if err != nil { return fmt.Errorf("--profile must be a number") } var series *sonarr.Series // Check if term is all digits (TVDB ID) if isAllDigits(term) { results, err := client.Lookup("tvdb:" + term) if err != nil { return err } if len(results) == 0 { return fmt.Errorf("No series found with TVDB ID %s", term) } series = &results[0] } else { results, err := client.Lookup(term) if err != nil { return err } if len(results) == 0 { return fmt.Errorf("No series found for '%s'", term) } // Rank results by similarity titles := make([]string, len(results)) for i, s := range results { titles[i] = s.Title } ranked := rankMatches(term, titles) if ranked[0].Similarity >= fuzzyThreshold { series = &results[ranked[0].Index] } else { if jsonOut { suggestions := jsonSuggestions{ Error: fmt.Sprintf("no match for '%s'", term), Suggestions: make([]jsonSuggestion, 0), } limit := 5 if len(ranked) < limit { limit = len(ranked) } for _, m := range ranked[:limit] { s := results[m.Index] suggestions.Suggestions = append(suggestions.Suggestions, jsonSuggestion{ Title: s.Title, Year: s.Year, TvdbID: s.TvdbID, }) } writeJSON(suggestions) return fmt.Errorf("no match found") } fmt.Fprintf(os.Stderr, "No match for '%s'. Did you mean:\n", term) limit := 5 if len(ranked) < limit { limit = len(ranked) } for _, m := range ranked[:limit] { s := results[m.Index] fmt.Fprintf(os.Stderr, " %s (%d) — TVDB ID: %d\n", s.Title, s.Year, s.TvdbID) } return fmt.Errorf("no match found") } } if series.ID != 0 { return fmt.Errorf("'%s' (%d) is already in your library", series.Title, series.Year) } series.QualityProfileID = profileID series.RootFolderPath = rootfolder series.Monitored = true series.AddOptions = &sonarr.AddOptions{SearchForMissingEpisodes: true} result, err := client.Add(*series) if err != nil { return err } if jsonOut { return writeJSON(jsonAdded{ Status: "added", Title: result.Title, Year: result.Year, TvdbID: result.TvdbID, }) } fmt.Printf("Added: %s (%d) — TVDB ID: %d\n", result.Title, result.Year, result.TvdbID) return nil } func TVSummary(client SonarrClient, term string, jsonOut bool) error { if term == "" { return fmt.Errorf("usage: arrman tv summary ") } var s *sonarr.Series if isAllDigits(term) { results, err := client.Lookup("tvdb:" + term) if err != nil { return err } if len(results) == 0 { return fmt.Errorf("No series found with TVDB ID %s", term) } s = &results[0] } else { results, err := client.Lookup(term) if err != nil { return err } if len(results) == 0 { return fmt.Errorf("No series found for '%s'", term) } // Use best fuzzy match titles := make([]string, len(results)) for i, r := range results { titles[i] = r.Title } ranked := rankMatches(term, titles) s = &results[ranked[0].Index] } if jsonOut { return writeJSON(seriesToJSON(*s)) } fmt.Printf("Title: %s\n", s.Title) fmt.Printf("Year: %d\n", s.Year) fmt.Printf("Status: %s\n", s.Status) if s.Network != "" { fmt.Printf("Network: %s\n", s.Network) } fmt.Printf("Seasons: %d\n", s.SeasonCount) if s.Statistics != nil { pct := 0.0 if s.Statistics.EpisodeCount > 0 { pct = float64(s.Statistics.EpisodeFileCount) / float64(s.Statistics.EpisodeCount) * 100 } fmt.Printf("Episodes: %d/%d (%.0f%%)\n", s.Statistics.EpisodeFileCount, s.Statistics.EpisodeCount, pct) } if s.NextAiring != "" { fmt.Printf("Next Airing: %s\n", s.NextAiring) } if s.Added != "" { fmt.Printf("Added: %s\n", s.Added) } if s.Overview != "" { fmt.Printf("Overview: %s\n", s.Overview) } return nil } func TVProfiles(client SonarrClient, jsonOut bool) error { profiles, err := client.QualityProfiles() if err != nil { return err } if jsonOut { out := make([]jsonProfile, len(profiles)) for i, p := range profiles { out[i] = jsonProfile{ID: p.ID, Name: p.Name} } return writeJSON(out) } for _, p := range profiles { fmt.Printf("ID: %d — Name: %s\n", p.ID, p.Name) } return nil } func TVRootFolders(client SonarrClient, jsonOut bool) error { folders, err := client.RootFolders() if err != nil { return err } if jsonOut { out := make([]jsonRootFolder, len(folders)) for i, f := range folders { out[i] = jsonRootFolder{ID: f.ID, Path: f.Path, FreeSpace: f.FreeSpace, FreeHuman: formatBytes(f.FreeSpace)} } return writeJSON(out) } for _, f := range folders { fmt.Printf("ID: %d — Path: %s — Free: %s\n", f.ID, f.Path, formatBytes(f.FreeSpace)) } return nil } func seriesToJSON(s sonarr.Series) jsonSeries { js := jsonSeries{ Title: s.Title, Year: s.Year, Status: s.Status, TvdbID: s.TvdbID, Network: s.Network, SeasonCount: s.SeasonCount, NextAiring: s.NextAiring, Overview: s.Overview, Added: s.Added, } if s.Statistics != nil { js.EpisodesHave = s.Statistics.EpisodeFileCount js.EpisodesTotal = s.Statistics.EpisodeCount } return js }