354 lines
10 KiB
Go
354 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/TopherMayor/unified-media-manager/internal/db"
|
|
"github.com/TopherMayor/unified-media-manager/internal/download"
|
|
)
|
|
|
|
type DownloadClientConfig struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Implementation string `json:"implementation"`
|
|
URL string `json:"url"`
|
|
APIKey *string `json:"-"`
|
|
Category string `json:"category"`
|
|
Priority int `json:"priority"`
|
|
Protocol string `json:"protocol"`
|
|
Settings json.RawMessage `json:"settings"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type DownloadClientConfigResponse struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Implementation string `json:"implementation"`
|
|
URL string `json:"url"`
|
|
Category string `json:"category"`
|
|
Priority int `json:"priority"`
|
|
Protocol string `json:"protocol"`
|
|
Settings json.RawMessage `json:"settings"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type DownloadClientWithInfo struct {
|
|
Client download.DownloadClient
|
|
Config DownloadClientConfig
|
|
}
|
|
|
|
type CreateDownloadClientRequest struct {
|
|
Name string `json:"name"`
|
|
Implementation string `json:"implementation"`
|
|
URL string `json:"url"`
|
|
APIKey *string `json:"api_key,omitempty"`
|
|
Category string `json:"category,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
Protocol string `json:"protocol,omitempty"`
|
|
Settings json.RawMessage `json:"settings,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
}
|
|
|
|
type UpdateDownloadClientRequest struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Implementation *string `json:"implementation,omitempty"`
|
|
URL *string `json:"url,omitempty"`
|
|
APIKey *string `json:"api_key,omitempty"`
|
|
Category *string `json:"category,omitempty"`
|
|
Priority *int `json:"priority,omitempty"`
|
|
Protocol *string `json:"protocol,omitempty"`
|
|
Settings json.RawMessage `json:"settings,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
}
|
|
|
|
type DownloadClientTestResult struct {
|
|
Success bool `json:"success"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
const downloadClientColumns = `id, name, implementation, url, api_key, category, priority, protocol, settings, enabled, created_at, updated_at`
|
|
|
|
type DownloadClientService struct {
|
|
db *db.DB
|
|
}
|
|
|
|
func NewDownloadClientService(database *db.DB) *DownloadClientService {
|
|
return &DownloadClientService{db: database}
|
|
}
|
|
|
|
func scanDownloadClientConfig(scanner interface{ Scan(...interface{}) error }) (*DownloadClientConfig, error) {
|
|
var cfg DownloadClientConfig
|
|
var apiKey sql.NullString
|
|
var settings []byte
|
|
|
|
err := scanner.Scan(&cfg.ID, &cfg.Name, &cfg.Implementation, &cfg.URL, &apiKey,
|
|
&cfg.Category, &cfg.Priority, &cfg.Protocol, &settings,
|
|
&cfg.Enabled, &cfg.CreatedAt, &cfg.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if apiKey.Valid {
|
|
cfg.APIKey = &apiKey.String
|
|
}
|
|
cfg.Settings = json.RawMessage(settings)
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
func clientConfigToResponse(cfg *DownloadClientConfig) DownloadClientConfigResponse {
|
|
return DownloadClientConfigResponse{
|
|
ID: cfg.ID,
|
|
Name: cfg.Name,
|
|
Implementation: cfg.Implementation,
|
|
URL: cfg.URL,
|
|
Category: cfg.Category,
|
|
Priority: cfg.Priority,
|
|
Protocol: cfg.Protocol,
|
|
Settings: cfg.Settings,
|
|
Enabled: cfg.Enabled,
|
|
CreatedAt: cfg.CreatedAt,
|
|
UpdatedAt: cfg.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func (s *DownloadClientService) List(ctx context.Context) ([]DownloadClientConfigResponse, error) {
|
|
rows, err := s.db.Pool.Query(ctx,
|
|
fmt.Sprintf("SELECT %s FROM download_clients ORDER BY priority, name", downloadClientColumns))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list download clients: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var items []DownloadClientConfigResponse
|
|
for rows.Next() {
|
|
cfg, err := scanDownloadClientConfig(rows)
|
|
if err != nil {
|
|
slog.Error("failed to scan download client", "error", err)
|
|
continue
|
|
}
|
|
items = append(items, clientConfigToResponse(cfg))
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
func (s *DownloadClientService) GetByID(ctx context.Context, id int64) (*DownloadClientConfig, error) {
|
|
row := s.db.Pool.QueryRow(ctx,
|
|
fmt.Sprintf("SELECT %s FROM download_clients WHERE id = $1", downloadClientColumns), id)
|
|
|
|
cfg, err := scanDownloadClientConfig(row)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("download client not found")
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func (s *DownloadClientService) Create(ctx context.Context, req CreateDownloadClientRequest) (int64, error) {
|
|
category := req.Category
|
|
if category == "" {
|
|
category = "umm"
|
|
}
|
|
protocol := req.Protocol
|
|
if protocol == "" {
|
|
switch req.Implementation {
|
|
case "sabnzbd":
|
|
protocol = "nzb"
|
|
case "qbittorrent":
|
|
protocol = "torrent"
|
|
default:
|
|
protocol = "nzb"
|
|
}
|
|
}
|
|
settings := req.Settings
|
|
if settings == nil {
|
|
settings = json.RawMessage("{}")
|
|
}
|
|
enabled := true
|
|
if req.Enabled != nil {
|
|
enabled = *req.Enabled
|
|
}
|
|
priority := 0
|
|
if req.Priority != nil {
|
|
priority = *req.Priority
|
|
}
|
|
|
|
var id int64
|
|
err := s.db.Pool.QueryRow(ctx,
|
|
`INSERT INTO download_clients (name, implementation, url, api_key, category, priority, protocol, settings, enabled)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
|
|
req.Name, req.Implementation, req.URL, req.APIKey, category, priority, protocol, settings, enabled).Scan(&id)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("create download client: %w", err)
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (s *DownloadClientService) Update(ctx context.Context, id int64, req UpdateDownloadClientRequest) error {
|
|
var setClauses []string
|
|
var args []interface{}
|
|
idx := 1
|
|
|
|
addCol := func(col string, val interface{}) {
|
|
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, idx))
|
|
args = append(args, val)
|
|
idx++
|
|
}
|
|
|
|
if req.Name != nil {
|
|
addCol("name", *req.Name)
|
|
}
|
|
if req.Implementation != nil {
|
|
addCol("implementation", *req.Implementation)
|
|
}
|
|
if req.URL != nil {
|
|
addCol("url", *req.URL)
|
|
}
|
|
if req.APIKey != nil {
|
|
addCol("api_key", *req.APIKey)
|
|
}
|
|
if req.Category != nil {
|
|
addCol("category", *req.Category)
|
|
}
|
|
if req.Priority != nil {
|
|
addCol("priority", *req.Priority)
|
|
}
|
|
if req.Protocol != nil {
|
|
addCol("protocol", *req.Protocol)
|
|
}
|
|
if req.Settings != nil {
|
|
addCol("settings", req.Settings)
|
|
}
|
|
if req.Enabled != nil {
|
|
addCol("enabled", *req.Enabled)
|
|
}
|
|
|
|
if len(setClauses) == 0 {
|
|
return fmt.Errorf("no fields to update")
|
|
}
|
|
|
|
addCol("updated_at", time.Now())
|
|
|
|
query := fmt.Sprintf("UPDATE download_clients SET %s WHERE id = $%d",
|
|
strings.Join(setClauses, ", "), idx)
|
|
args = append(args, id)
|
|
|
|
tag, err := s.db.Pool.Exec(ctx, query, args...)
|
|
if err != nil {
|
|
return fmt.Errorf("update download client: %w", err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return fmt.Errorf("download client not found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *DownloadClientService) Delete(ctx context.Context, id int64) error {
|
|
tag, err := s.db.Pool.Exec(ctx, "DELETE FROM download_clients WHERE id = $1", id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete download client: %w", err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return fmt.Errorf("download client not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DownloadClientService) GetClient(ctx context.Context, protocol string) (download.DownloadClient, *DownloadClientConfig, error) {
|
|
row := s.db.Pool.QueryRow(ctx,
|
|
fmt.Sprintf("SELECT %s FROM download_clients WHERE enabled = true AND protocol = $1 ORDER BY priority ASC LIMIT 1", downloadClientColumns),
|
|
protocol)
|
|
|
|
cfg, err := scanDownloadClientConfig(row)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("no enabled download client for protocol: %s", protocol)
|
|
}
|
|
|
|
client, err := s.instantiateClient(cfg)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return client, cfg, nil
|
|
}
|
|
|
|
func (s *DownloadClientService) GetAllEnabled(ctx context.Context, protocol string) ([]DownloadClientWithInfo, error) {
|
|
rows, err := s.db.Pool.Query(ctx,
|
|
fmt.Sprintf("SELECT %s FROM download_clients WHERE enabled = true AND protocol = $1 ORDER BY priority ASC", downloadClientColumns),
|
|
protocol)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list enabled download clients: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var clients []DownloadClientWithInfo
|
|
for rows.Next() {
|
|
cfg, err := scanDownloadClientConfig(rows)
|
|
if err != nil {
|
|
slog.Error("failed to scan download client", "error", err)
|
|
continue
|
|
}
|
|
|
|
client, err := s.instantiateClient(cfg)
|
|
if err != nil {
|
|
slog.Error("failed to instantiate download client", "error", err, "name", cfg.Name)
|
|
continue
|
|
}
|
|
|
|
clients = append(clients, DownloadClientWithInfo{
|
|
Client: client,
|
|
Config: *cfg,
|
|
})
|
|
}
|
|
|
|
return clients, nil
|
|
}
|
|
|
|
func (s *DownloadClientService) Test(ctx context.Context, id int64) (*DownloadClientTestResult, error) {
|
|
cfg, err := s.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := s.instantiateClient(cfg)
|
|
if err != nil {
|
|
return &DownloadClientTestResult{Success: false, Error: err.Error()}, nil
|
|
}
|
|
|
|
_, err = client.GetCompleted(ctx)
|
|
if err != nil {
|
|
return &DownloadClientTestResult{Success: false, Error: err.Error()}, nil
|
|
}
|
|
|
|
return &DownloadClientTestResult{Success: true}, nil
|
|
}
|
|
|
|
func (s *DownloadClientService) instantiateClient(cfg *DownloadClientConfig) (download.DownloadClient, error) {
|
|
apiKey := ""
|
|
if cfg.APIKey != nil {
|
|
apiKey = *cfg.APIKey
|
|
}
|
|
|
|
switch cfg.Implementation {
|
|
case "sabnzbd":
|
|
return download.NewSABnzbdClient(cfg.URL, apiKey), nil
|
|
case "qbittorrent":
|
|
return download.NewQBittorrentClient(cfg.URL, apiKey), nil
|
|
default:
|
|
return nil, fmt.Errorf("unknown download client implementation: %s", cfg.Implementation)
|
|
}
|
|
}
|