Add initial implementation of *arr service client

- Created `client.go` for the main client structure and methods to interact with *arr services.
- Added `types.go` to define data structures for system status, movies, and series.
- Implemented `radarr.go` for Radarr-specific client methods including health checks and movie retrieval.
- Introduced `interfaces.go` to define service interfaces for common operations across *arr services.
- Established a basic `main.go` for application entry point.
- Included a tutorial markdown file to guide users through building the client and understanding Go concepts.
- Initialized `go.mod` for module management.
- Organized code into appropriate packages for better structure and maintainability.
This commit is contained in:
2025-08-21 18:52:15 -04:00
commit 6240ed0b1f
7 changed files with 3009 additions and 0 deletions

68
client.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Client struct {
BaseURL string
APIKey string
HTTPClient *http.Client
}
func NewClient(baseURL, apiKey string) *Client {
return &Client{
BaseURL: baseURL,
APIKey: apiKey,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *Client) GetSystemStatus() (*SystemStatus, error) {
// Create the URL for the system status endpoint.
url := fmt.Sprintf("%s/api/v3/system/status", c.BaseURL)
// Create a new GET request to the system status endpoint.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set the API key and content type headers.
req.Header.Set("X-Api-Key", c.APIKey)
req.Header.Set("Content-Type", "application/json")
// Send the request and get the response.
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Check if the response is successful.
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get system status: %s", resp.Status)
}
// Read the response body.
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Unmarshal the response body into a SystemStatus struct.
var status SystemStatus
if err := json.Unmarshal(body, &status); err != nil {
return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
}
// Return the SystemStatus struct.
return &status, nil
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/MikeyYeahYeah/arr-go-client
go 1.24.1

2732
go_arr_tutorial.md Normal file

File diff suppressed because it is too large Load Diff

30
interfaces.go Normal file
View File

@@ -0,0 +1,30 @@
package main
// ServiceClient defines the interface all *arr services must implement
type ServiceClient interface {
GetSystemStatus() (*SystemStatus, error)
GetHealth() ([]HealthCheck, error)
}
// MovieService defines movie-specific operations (Radarr)
type MovieService interface {
ServiceClient
GetMovies() ([]Movie, error)
GetMovie(id int) (*Movie, error)
AddMovie(movie *Movie) (*Movie, error)
}
// SeriesService defines series-specific operations (Sonarr)
type SeriesService interface {
ServiceClient
GetSeries() ([]Series, error)
GetSeriesById(id int) (*Series, error)
AddSeries(series *Series) (*Series, error)
}
// HealthCheck is the struct that represents the health check result.
type HealthCheck struct {
Source string `json:"source"`
Type string `json:"type"`
Message string `json:"message"`
}

7
main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}

124
radarr.go Normal file
View File

@@ -0,0 +1,124 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type RadarrClient struct {
*Client
}
func NewRadarrClient(baseURL, apiKey string) *RadarrClient {
return &RadarrClient{
Client: NewClient(baseURL, apiKey),
}
}
func (r *RadarrClient) GetHealth() ([]HealthCheck, error) {
// Create the URL for the health endpoint.
url := fmt.Sprintf("%s/api/v3/health", r.BaseURL)
// Create a new GET request to the health endpoint.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set the API key and content type headers.
req.Header.Set("X-Api-Key", r.APIKey)
req.Header.Set("Content-Type", "application/json")
// Send the request and get the response.
resp, err := r.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Check if the response is successful.
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get health: %s", resp.Status)
}
// Decode the response body into a slice of HealthCheck structs.
var health []HealthCheck
if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}
return health, nil
}
func (r *RadarrClient) GetMovies() ([]Movie, error) {
// Create the URL for the movies endpoint.
url := fmt.Sprintf("%s/api/v3/movie", r.BaseURL)
// Create a new GET request to the movies endpoint.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set the API key and content type headers.
req.Header.Set("X-Api-Key", r.APIKey)
req.Header.Set("Content-Type", "application/json")
// Send the request and get the response.
resp, err := r.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Check if the response is successful.
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get movies: %s", resp.Status)
}
// Decode the response body into a slice of Movie structs.
var movies []Movie
if err := json.NewDecoder(resp.Body).Decode(&movies); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}
return movies, nil
}
// GetMovie retrieves a specific movie by ID.
func (r *RadarrClient) GetMovie(id int) (*Movie, error) {
// Create the URL for the movie endpoint.
url := fmt.Sprintf("%s/api/v3/movie/%d", r.BaseURL, id)
// Create a new GET request to the movie endpoint.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set the API key and content type headers.
req.Header.Set("X-Api-Key", r.APIKey)
req.Header.Set("Content-Type", "application/json")
// Send the request and get the response.
resp, err := r.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Check if the response is successful.
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get movie: %s", resp.Status)
}
// Decode the response body into a Movie struct.
var movie Movie
if err := json.NewDecoder(resp.Body).Decode(&movie); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}
return &movie, nil
}

45
types.go Normal file
View File

@@ -0,0 +1,45 @@
package main
import "time"
type SystemStatus struct {
Version string `json:"version"`
BuildTime string `json:"buildTime"`
IsDebug bool `json:"isDebug"`
IsProduction bool `json:"isProduction"`
IsAdmin bool `json:"isAdmin"`
IsUserInteractive bool `json:"isUserInteractive"`
StartupPath string `json:"startupPath"`
AppData string `json:"appData"`
OsName string `json:"osName"`
OSVersion string `json:"osVersion"`
}
type Movie struct {
ID int `json:"id"`
Title string `json:"title"`
Year int `json:"year"`
Path string `json:"path"`
Monitored bool `json:"monitored"`
Added time.Time `json:"added"`
QualityProfileID int `json:"qualityProfileId"`
ImdbID string `json:"imdbId"`
RmdbID int `json:"tmdbId"`
}
type Series struct {
ID int `json:"id"`
Title string `json:"title"`
Year int `json:"year"`
Path string `json:"path"`
Monitored bool `json:"monitored"`
Added time.Time `json:"added"`
QualityProfileID int `json:"qualityProfileId"`
TvdbID int `json:"tvdbId"`
Seasons []Season `json:"seasons"`
}
type Season struct {
SeasonNumber int `json:"seasonNumber"`
Monitored bool `json:"monitored"`
}