Changelog 0.3.0
This commit is contained in:
120
pkg/arr/arr.go
Normal file
120
pkg/arr/arr.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package arr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"goBlack/common"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Type is a type of arr
|
||||
type Type string
|
||||
|
||||
const (
|
||||
Sonarr Type = "sonarr"
|
||||
Radarr Type = "radarr"
|
||||
Lidarr Type = "lidarr"
|
||||
Readarr Type = "readarr"
|
||||
)
|
||||
|
||||
var (
|
||||
client *common.RLHTTPClient = common.NewRLHTTPClient(nil, nil)
|
||||
logger *log.Logger = common.NewLogger("QBit", os.Stdout)
|
||||
)
|
||||
|
||||
type Arr struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Token string `json:"token"`
|
||||
Type Type `json:"type"`
|
||||
}
|
||||
|
||||
func NewArr(name, host, token string, arrType Type) *Arr {
|
||||
return &Arr{
|
||||
Name: name,
|
||||
Host: host,
|
||||
Token: token,
|
||||
Type: arrType,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Arr) Request(method, endpoint string, payload interface{}) (*http.Response, error) {
|
||||
if a.Token == "" || a.Host == "" {
|
||||
return nil, nil
|
||||
}
|
||||
url, err := common.JoinURL(a.Host, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var jsonPayload []byte
|
||||
|
||||
if payload != nil {
|
||||
jsonPayload, err = json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonPayload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Api-Key", a.Token)
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Arrs map[string]*Arr // name -> arr
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func inferType(host, name string) Type {
|
||||
switch {
|
||||
case strings.Contains(host, "sonarr") || strings.Contains(name, "sonarr"):
|
||||
return Sonarr
|
||||
case strings.Contains(host, "radarr") || strings.Contains(name, "radarr"):
|
||||
return Radarr
|
||||
case strings.Contains(host, "lidarr") || strings.Contains(name, "lidarr"):
|
||||
return Lidarr
|
||||
case strings.Contains(host, "readarr") || strings.Contains(name, "readarr"):
|
||||
return Readarr
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func NewStorage() *Storage {
|
||||
arrs := make(map[string]*Arr)
|
||||
//for name, arrCfg := range cfg {
|
||||
// arrs[name] = NewArr(name, arrCfg.Host, arrCfg.Token, inferType(arrCfg.Host, name))
|
||||
//}
|
||||
return &Storage{
|
||||
Arrs: arrs,
|
||||
}
|
||||
}
|
||||
|
||||
func (as *Storage) AddOrUpdate(arr *Arr) {
|
||||
as.mu.Lock()
|
||||
defer as.mu.Unlock()
|
||||
as.Arrs[arr.Host] = arr
|
||||
}
|
||||
|
||||
func (as *Storage) Get(name string) *Arr {
|
||||
as.mu.RLock()
|
||||
defer as.mu.RUnlock()
|
||||
return as.Arrs[name]
|
||||
}
|
||||
|
||||
func (as *Storage) GetAll() []*Arr {
|
||||
as.mu.RLock()
|
||||
defer as.mu.RUnlock()
|
||||
arrs := make([]*Arr, 0, len(as.Arrs))
|
||||
for _, arr := range as.Arrs {
|
||||
arrs = append(arrs, arr)
|
||||
}
|
||||
return arrs
|
||||
}
|
||||
29
pkg/arr/content.go
Normal file
29
pkg/arr/content.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package arr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ContentRequest struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"name"`
|
||||
Arr string `json:"arr"`
|
||||
}
|
||||
|
||||
func (a *Arr) GetContents() *ContentRequest {
|
||||
resp, err := a.Request(http.MethodGet, "api/v3/series", nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data *ContentRequest
|
||||
if err = json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Data: %v\n", data)
|
||||
//data.Arr = a.Name
|
||||
return data
|
||||
}
|
||||
41
pkg/arr/history.go
Normal file
41
pkg/arr/history.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package arr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
gourl "net/url"
|
||||
)
|
||||
|
||||
type HistorySchema struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
SortKey string `json:"sortKey"`
|
||||
SortDirection string `json:"sortDirection"`
|
||||
TotalRecords int `json:"totalRecords"`
|
||||
Records []struct {
|
||||
ID int `json:"id"`
|
||||
DownloadID string `json:"downloadId"`
|
||||
} `json:"records"`
|
||||
}
|
||||
|
||||
func (a *Arr) GetHistory(downloadId, eventType string) *HistorySchema {
|
||||
query := gourl.Values{}
|
||||
if downloadId != "" {
|
||||
query.Add("downloadId", downloadId)
|
||||
}
|
||||
query.Add("eventType", eventType)
|
||||
query.Add("pageSize", "100")
|
||||
url := "history" + "?" + query.Encode()
|
||||
resp, err := a.Request(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data *HistorySchema
|
||||
|
||||
if err = json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
|
||||
}
|
||||
209
pkg/arr/import.go
Normal file
209
pkg/arr/import.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package arr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
gourl "net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImportResponseSchema struct {
|
||||
Path string `json:"path"`
|
||||
RelativePath string `json:"relativePath"`
|
||||
FolderName string `json:"folderName"`
|
||||
Name string `json:"name"`
|
||||
Size int `json:"size"`
|
||||
Series struct {
|
||||
Title string `json:"title"`
|
||||
SortTitle string `json:"sortTitle"`
|
||||
Status string `json:"status"`
|
||||
Ended bool `json:"ended"`
|
||||
Overview string `json:"overview"`
|
||||
Network string `json:"network"`
|
||||
AirTime string `json:"airTime"`
|
||||
Images []struct {
|
||||
CoverType string `json:"coverType"`
|
||||
RemoteUrl string `json:"remoteUrl"`
|
||||
} `json:"images"`
|
||||
OriginalLanguage struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"originalLanguage"`
|
||||
Seasons []struct {
|
||||
SeasonNumber int `json:"seasonNumber"`
|
||||
Monitored bool `json:"monitored"`
|
||||
} `json:"seasons"`
|
||||
Year int `json:"year"`
|
||||
Path string `json:"path"`
|
||||
QualityProfileId int `json:"qualityProfileId"`
|
||||
SeasonFolder bool `json:"seasonFolder"`
|
||||
Monitored bool `json:"monitored"`
|
||||
MonitorNewItems string `json:"monitorNewItems"`
|
||||
UseSceneNumbering bool `json:"useSceneNumbering"`
|
||||
Runtime int `json:"runtime"`
|
||||
TvdbId int `json:"tvdbId"`
|
||||
TvRageId int `json:"tvRageId"`
|
||||
TvMazeId int `json:"tvMazeId"`
|
||||
TmdbId int `json:"tmdbId"`
|
||||
FirstAired time.Time `json:"firstAired"`
|
||||
LastAired time.Time `json:"lastAired"`
|
||||
SeriesType string `json:"seriesType"`
|
||||
CleanTitle string `json:"cleanTitle"`
|
||||
ImdbId string `json:"imdbId"`
|
||||
TitleSlug string `json:"titleSlug"`
|
||||
Certification string `json:"certification"`
|
||||
Genres []string `json:"genres"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
Added time.Time `json:"added"`
|
||||
Ratings struct {
|
||||
Votes int `json:"votes"`
|
||||
Value float64 `json:"value"`
|
||||
} `json:"ratings"`
|
||||
LanguageProfileId int `json:"languageProfileId"`
|
||||
Id int `json:"id"`
|
||||
} `json:"series"`
|
||||
SeasonNumber int `json:"seasonNumber"`
|
||||
Episodes []struct {
|
||||
SeriesId int `json:"seriesId"`
|
||||
TvdbId int `json:"tvdbId"`
|
||||
EpisodeFileId int `json:"episodeFileId"`
|
||||
SeasonNumber int `json:"seasonNumber"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Title string `json:"title"`
|
||||
AirDate string `json:"airDate"`
|
||||
AirDateUtc time.Time `json:"airDateUtc"`
|
||||
Runtime int `json:"runtime"`
|
||||
Overview string `json:"overview"`
|
||||
HasFile bool `json:"hasFile"`
|
||||
Monitored bool `json:"monitored"`
|
||||
AbsoluteEpisodeNumber int `json:"absoluteEpisodeNumber"`
|
||||
UnverifiedSceneNumbering bool `json:"unverifiedSceneNumbering"`
|
||||
Id int `json:"id"`
|
||||
FinaleType string `json:"finaleType,omitempty"`
|
||||
} `json:"episodes"`
|
||||
ReleaseGroup string `json:"releaseGroup"`
|
||||
Quality struct {
|
||||
Quality struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
Resolution int `json:"resolution"`
|
||||
} `json:"quality"`
|
||||
Revision struct {
|
||||
Version int `json:"version"`
|
||||
Real int `json:"real"`
|
||||
IsRepack bool `json:"isRepack"`
|
||||
} `json:"revision"`
|
||||
} `json:"quality"`
|
||||
Languages []struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"languages"`
|
||||
QualityWeight int `json:"qualityWeight"`
|
||||
CustomFormats []interface{} `json:"customFormats"`
|
||||
CustomFormatScore int `json:"customFormatScore"`
|
||||
IndexerFlags int `json:"indexerFlags"`
|
||||
ReleaseType string `json:"releaseType"`
|
||||
Rejections []struct {
|
||||
Reason string `json:"reason"`
|
||||
Type string `json:"type"`
|
||||
} `json:"rejections"`
|
||||
Id int `json:"id"`
|
||||
}
|
||||
|
||||
type ManualImportRequestFile struct {
|
||||
Path string `json:"path"`
|
||||
SeriesId int `json:"seriesId"`
|
||||
SeasonNumber int `json:"seasonNumber"`
|
||||
EpisodeIds []int `json:"episodeIds"`
|
||||
Quality struct {
|
||||
Quality struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
Resolution int `json:"resolution"`
|
||||
} `json:"quality"`
|
||||
Revision struct {
|
||||
Version int `json:"version"`
|
||||
Real int `json:"real"`
|
||||
IsRepack bool `json:"isRepack"`
|
||||
} `json:"revision"`
|
||||
} `json:"quality"`
|
||||
Languages []struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"languages"`
|
||||
ReleaseGroup string `json:"releaseGroup"`
|
||||
CustomFormats []interface{} `json:"customFormats"`
|
||||
CustomFormatScore int `json:"customFormatScore"`
|
||||
IndexerFlags int `json:"indexerFlags"`
|
||||
ReleaseType string `json:"releaseType"`
|
||||
Rejections []struct {
|
||||
Reason string `json:"reason"`
|
||||
Type string `json:"type"`
|
||||
} `json:"rejections"`
|
||||
}
|
||||
|
||||
type ManualImportRequestSchema struct {
|
||||
Name string `json:"name"`
|
||||
Files []ManualImportRequestFile `json:"files"`
|
||||
ImportMode string `json:"importMode"`
|
||||
}
|
||||
|
||||
func (a *Arr) Import(path string, seriesId int, seasons []int) (io.ReadCloser, error) {
|
||||
query := gourl.Values{}
|
||||
query.Add("folder", path)
|
||||
if seriesId != 0 {
|
||||
query.Add("seriesId", strconv.Itoa(seriesId))
|
||||
}
|
||||
url := "api/v3/manualimport" + "?" + query.Encode()
|
||||
resp, err := a.Request(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to import, invalid file: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data []ImportResponseSchema
|
||||
if err = json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
var files []ManualImportRequestFile
|
||||
for _, d := range data {
|
||||
episodesIds := []int{}
|
||||
for _, e := range d.Episodes {
|
||||
episodesIds = append(episodesIds, e.Id)
|
||||
}
|
||||
file := ManualImportRequestFile{
|
||||
Path: d.Path,
|
||||
SeriesId: d.Series.Id,
|
||||
SeasonNumber: d.SeasonNumber,
|
||||
EpisodeIds: episodesIds,
|
||||
Quality: d.Quality,
|
||||
Languages: d.Languages,
|
||||
ReleaseGroup: d.ReleaseGroup,
|
||||
CustomFormats: d.CustomFormats,
|
||||
CustomFormatScore: d.CustomFormatScore,
|
||||
IndexerFlags: d.IndexerFlags,
|
||||
ReleaseType: d.ReleaseType,
|
||||
Rejections: d.Rejections,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
request := ManualImportRequestSchema{
|
||||
Name: "ManualImport",
|
||||
Files: files,
|
||||
ImportMode: "copy",
|
||||
}
|
||||
|
||||
url = "api/v3/command"
|
||||
resp, err = a.Request(http.MethodPost, url, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to import: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.Body, nil
|
||||
|
||||
}
|
||||
54
pkg/arr/refresh.go
Normal file
54
pkg/arr/refresh.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package arr
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"goBlack/common"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (a *Arr) Refresh() error {
|
||||
payload := map[string]string{"name": "RefreshMonitoredDownloads"}
|
||||
|
||||
resp, err := a.Request(http.MethodPost, "api/v3/command", payload)
|
||||
if err == nil && resp != nil {
|
||||
statusOk := strconv.Itoa(resp.StatusCode)[0] == '2'
|
||||
if statusOk {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to refresh monitored downloads for %s", cmp.Or(a.Name, a.Host))
|
||||
}
|
||||
|
||||
func (a *Arr) MarkAsFailed(infoHash string) error {
|
||||
downloadId := strings.ToUpper(infoHash)
|
||||
history := a.GetHistory(downloadId, "grabbed")
|
||||
if history == nil {
|
||||
return nil
|
||||
}
|
||||
torrentId := 0
|
||||
for _, record := range history.Records {
|
||||
if strings.EqualFold(record.DownloadID, downloadId) {
|
||||
torrentId = record.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if torrentId != 0 {
|
||||
url, err := common.JoinURL(a.Host, "history/failed/", strconv.Itoa(torrentId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := &http.Client{}
|
||||
_, err = client.Do(req)
|
||||
if err == nil {
|
||||
return fmt.Errorf("failed to mark %s as failed: %v", cmp.Or(a.Name, a.Host), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
31
pkg/arr/tmdb.go
Normal file
31
pkg/arr/tmdb.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package arr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
url2 "net/url"
|
||||
)
|
||||
|
||||
type TMDBResponse struct {
|
||||
Page int `json:"page"`
|
||||
Results []struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MediaType string `json:"media_type"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
func SearchTMDB(term string) (*TMDBResponse, error) {
|
||||
resp, err := http.Get("https://api.themoviedb.org/3/search/multi?api_key=key&query=" + url2.QueryEscape(term))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data *TMDBResponse
|
||||
if err = json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user