336 lines
7.6 KiB
Go
336 lines
7.6 KiB
Go
package arr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"golang.org/x/sync/errgroup"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type episode struct {
|
|
Id int `json:"id"`
|
|
EpisodeFileID int `json:"episodeFileId"`
|
|
}
|
|
|
|
type sonarrSearch struct {
|
|
Name string `json:"name"`
|
|
SeasonNumber int `json:"seasonNumber"`
|
|
SeriesId int `json:"seriesId"`
|
|
}
|
|
|
|
type radarrSearch struct {
|
|
Name string `json:"name"`
|
|
MovieIds []int `json:"movieIds"`
|
|
}
|
|
|
|
func (a *Arr) GetMedia(mediaId string) ([]Content, error) {
|
|
// Get series
|
|
if a.Type == Radarr {
|
|
return GetMovies(a, mediaId)
|
|
}
|
|
// This is likely Sonarr
|
|
resp, err := a.Request(http.MethodGet, fmt.Sprintf("api/v3/series?tvdbId=%s", mediaId), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
// This is likely Radarr
|
|
return GetMovies(a, mediaId)
|
|
}
|
|
a.Type = Sonarr
|
|
|
|
type series struct {
|
|
Title string `json:"title"`
|
|
Id int `json:"id"`
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("failed to get series: %s", resp.Status)
|
|
}
|
|
var data []series
|
|
if err = json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
return nil, fmt.Errorf("failed to decode series: %v", err)
|
|
}
|
|
// Get series files
|
|
contents := make([]Content, 0)
|
|
for _, d := range data {
|
|
resp, err = a.Request(http.MethodGet, fmt.Sprintf("api/v3/episodefile?seriesId=%d", d.Id), nil)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var ct Content
|
|
var seriesFiles []seriesFile
|
|
episodeFileIDMap := make(map[int]int)
|
|
func() {
|
|
defer resp.Body.Close()
|
|
if err = json.NewDecoder(resp.Body).Decode(&seriesFiles); err != nil {
|
|
return
|
|
}
|
|
ct = Content{
|
|
Title: d.Title,
|
|
Id: d.Id,
|
|
}
|
|
}()
|
|
resp, err = a.Request(http.MethodGet, fmt.Sprintf("api/v3/episode?seriesId=%d", d.Id), nil)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
func() {
|
|
defer resp.Body.Close()
|
|
var episodes []episode
|
|
if err = json.NewDecoder(resp.Body).Decode(&episodes); err != nil {
|
|
return
|
|
}
|
|
|
|
for _, e := range episodes {
|
|
episodeFileIDMap[e.EpisodeFileID] = e.Id
|
|
}
|
|
}()
|
|
files := make([]ContentFile, 0)
|
|
for _, file := range seriesFiles {
|
|
eId, ok := episodeFileIDMap[file.Id]
|
|
if !ok {
|
|
eId = 0
|
|
}
|
|
if file.Id == 0 || file.Path == "" {
|
|
// Skip files without path
|
|
continue
|
|
}
|
|
files = append(files, ContentFile{
|
|
FileId: file.Id,
|
|
Path: file.Path,
|
|
Id: d.Id,
|
|
EpisodeId: eId,
|
|
SeasonNumber: file.SeasonNumber,
|
|
Size: file.Size,
|
|
})
|
|
}
|
|
if len(files) == 0 {
|
|
// Skip series without files
|
|
continue
|
|
}
|
|
ct.Files = files
|
|
contents = append(contents, ct)
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
func GetMovies(a *Arr, tvId string) ([]Content, error) {
|
|
resp, err := a.Request(http.MethodGet, fmt.Sprintf("api/v3/movie?tmdbId=%s", tvId), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
// This is likely Lidarr or Readarr
|
|
return nil, fmt.Errorf("failed to get movies: %s", resp.Status)
|
|
}
|
|
a.Type = Radarr
|
|
defer resp.Body.Close()
|
|
var movies []Movie
|
|
if err = json.NewDecoder(resp.Body).Decode(&movies); err != nil {
|
|
return nil, fmt.Errorf("failed to decode movies: %v", err)
|
|
}
|
|
contents := make([]Content, 0)
|
|
for _, movie := range movies {
|
|
if movie.MovieFile.Id == 0 || movie.MovieFile.Path == "" {
|
|
// Skip movies without files
|
|
continue
|
|
}
|
|
ct := Content{
|
|
Title: movie.Title,
|
|
Id: movie.Id,
|
|
}
|
|
files := make([]ContentFile, 0)
|
|
|
|
files = append(files, ContentFile{
|
|
FileId: movie.MovieFile.Id,
|
|
Id: movie.Id,
|
|
Path: movie.MovieFile.Path,
|
|
Size: movie.MovieFile.Size,
|
|
})
|
|
ct.Files = files
|
|
contents = append(contents, ct)
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
// searchSonarr searches for missing files in the arr
|
|
// map ids are series id and season number
|
|
func (a *Arr) searchSonarr(files []ContentFile) error {
|
|
ids := make(map[string]any)
|
|
for _, f := range files {
|
|
// Join series id and season number
|
|
id := fmt.Sprintf("%d-%d", f.Id, f.SeasonNumber)
|
|
ids[id] = nil
|
|
}
|
|
|
|
g, ctx := errgroup.WithContext(context.Background())
|
|
|
|
// Limit concurrent goroutines
|
|
g.SetLimit(10)
|
|
for id := range ids {
|
|
id := id
|
|
g.Go(func() error {
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
parts := strings.Split(id, "-")
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf("invalid id: %s", id)
|
|
}
|
|
seriesId, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
seasonNumber, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload := sonarrSearch{
|
|
Name: "SeasonSearch",
|
|
SeasonNumber: seasonNumber,
|
|
SeriesId: seriesId,
|
|
}
|
|
resp, err := a.Request(http.MethodPost, "api/v3/command", payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to automatic search: %v", err)
|
|
}
|
|
if resp.StatusCode >= 300 || resp.StatusCode < 200 {
|
|
return fmt.Errorf("failed to automatic search. Status Code: %s", resp.Status)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Arr) searchRadarr(files []ContentFile) error {
|
|
ids := make([]int, 0)
|
|
for _, f := range files {
|
|
ids = append(ids, f.Id)
|
|
}
|
|
payload := radarrSearch{
|
|
Name: "MoviesSearch",
|
|
MovieIds: ids,
|
|
}
|
|
resp, err := a.Request(http.MethodPost, "api/v3/command", payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to automatic search: %v", err)
|
|
}
|
|
if statusOk := strconv.Itoa(resp.StatusCode)[0] == '2'; !statusOk {
|
|
return fmt.Errorf("failed to automatic search. Status Code: %s", resp.Status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Arr) SearchMissing(files []ContentFile) error {
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
return a.batchSearchMissing(files)
|
|
}
|
|
|
|
func (a *Arr) batchSearchMissing(files []ContentFile) error {
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
BatchSize := 50
|
|
// Batch search for missing files
|
|
if len(files) > BatchSize {
|
|
for i := 0; i < len(files); i += BatchSize {
|
|
end := i + BatchSize
|
|
if end > len(files) {
|
|
end = len(files)
|
|
}
|
|
if err := a.searchMissing(files[i:end]); err != nil {
|
|
// continue searching the rest of the files
|
|
continue
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return a.searchMissing(files)
|
|
}
|
|
|
|
func (a *Arr) searchMissing(files []ContentFile) error {
|
|
switch a.Type {
|
|
case Sonarr:
|
|
return a.searchSonarr(files)
|
|
case Radarr:
|
|
return a.searchRadarr(files)
|
|
default:
|
|
return fmt.Errorf("unknown arr type: %s", a.Type)
|
|
}
|
|
}
|
|
|
|
func (a *Arr) DeleteFiles(files []ContentFile) error {
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
BatchSize := 50
|
|
// Batch delete files
|
|
if len(files) > BatchSize {
|
|
for i := 0; i < len(files); i += BatchSize {
|
|
end := i + BatchSize
|
|
if end > len(files) {
|
|
end = len(files)
|
|
}
|
|
if err := a.batchDeleteFiles(files[i:end]); err != nil {
|
|
// continue deleting the rest of the files
|
|
continue
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return a.batchDeleteFiles(files)
|
|
}
|
|
|
|
func (a *Arr) batchDeleteFiles(files []ContentFile) error {
|
|
ids := make([]int, 0)
|
|
for _, f := range files {
|
|
ids = append(ids, f.FileId)
|
|
}
|
|
defer func() {
|
|
// Delete files, or at least try
|
|
for _, f := range files {
|
|
f.Delete()
|
|
}
|
|
}()
|
|
var payload interface{}
|
|
switch a.Type {
|
|
case Sonarr:
|
|
payload = struct {
|
|
EpisodeFileIds []int `json:"episodeFileIds"`
|
|
}{
|
|
EpisodeFileIds: ids,
|
|
}
|
|
_, err := a.Request(http.MethodDelete, "api/v3/episodefile/bulk", payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case Radarr:
|
|
payload = struct {
|
|
MovieFileIds []int `json:"movieFileIds"`
|
|
}{
|
|
MovieFileIds: ids,
|
|
}
|
|
_, err := a.Request(http.MethodDelete, "api/v3/moviefile/bulk", payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown arr type: %s", a.Type)
|
|
}
|
|
return nil
|
|
}
|