Sync from /srv/compose/unified-media-manager
This commit is contained in:
353
internal/service/download_client.go
Normal file
353
internal/service/download_client.go
Normal file
@@ -0,0 +1,353 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user