294 lines
6.9 KiB
Go
294 lines
6.9 KiB
Go
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 <title or TVDB ID>")
|
|
}
|
|
|
|
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
|
|
}
|