Files
arrman/cmd/tv.go
T
2026-03-12 22:13:57 +00:00

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
}