Files
decypharr/pkg/sabnzbd/handlers.go
2025-08-01 15:27:24 +01:00

477 lines
12 KiB
Go

package sabnzbd
import (
"context"
"fmt"
"github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/arr"
"github.com/sirrobot01/decypharr/pkg/usenet"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// handleAPI is the main handler for all SABnzbd API requests
func (s *SABnzbd) handleAPI(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
mode := getMode(ctx)
switch mode {
case ModeQueue:
s.handleQueue(w, r)
case ModeHistory:
s.handleHistory(w, r)
case ModeConfig:
s.handleConfig(w, r)
case ModeStatus, ModeFullStatus:
s.handleStatus(w, r)
case ModeGetConfig:
s.handleConfig(w, r)
case ModeAddURL:
s.handleAddURL(w, r)
case ModeAddFile:
s.handleAddFile(w, r)
case ModeVersion:
s.handleVersion(w, r)
case ModeGetCats:
s.handleGetCategories(w, r)
case ModeGetScripts:
s.handleGetScripts(w, r)
case ModeGetFiles:
s.handleGetFiles(w, r)
default:
// Default to queue if no mode specified
s.logger.Warn().Str("mode", mode).Msg("Unknown API mode, returning 404")
http.Error(w, "Not Found", http.StatusNotFound)
}
}
func (s *SABnzbd) handleQueue(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
if name == "" {
s.handleListQueue(w, r)
return
}
name = strings.ToLower(strings.TrimSpace(name))
switch name {
case "delete":
s.handleQueueDelete(w, r)
case "pause":
s.handleQueuePause(w, r)
case "resume":
s.handleQueueResume(w, r)
}
}
// handleResume handles resume operations
func (s *SABnzbd) handleQueueResume(w http.ResponseWriter, r *http.Request) {
response := StatusResponse{Status: true}
request.JSONResponse(w, response, http.StatusOK)
}
// handleDelete handles delete operations
func (s *SABnzbd) handleQueueDelete(w http.ResponseWriter, r *http.Request) {
nzoIDs := r.FormValue("value")
if nzoIDs == "" {
s.writeError(w, "No NZB IDs provided", http.StatusBadRequest)
return
}
var successCount int
var errors []string
for _, nzoID := range strings.Split(nzoIDs, ",") {
nzoID = strings.TrimSpace(nzoID)
if nzoID == "" {
continue // Skip empty IDs
}
s.logger.Info().Str("nzo_id", nzoID).Msg("Deleting NZB")
// Use atomic delete operation
if err := s.usenet.Store().AtomicDelete(nzoID); err != nil {
s.logger.Error().
Err(err).
Str("nzo_id", nzoID).
Msg("Failed to delete NZB")
errors = append(errors, fmt.Sprintf("Failed to delete %s: %v", nzoID, err))
} else {
successCount++
}
}
// Return response with success/error information
if len(errors) > 0 {
if successCount == 0 {
// All deletions failed
s.writeError(w, fmt.Sprintf("All deletions failed: %s", strings.Join(errors, "; ")), http.StatusInternalServerError)
return
} else {
// Partial success
s.logger.Warn().
Int("success_count", successCount).
Int("error_count", len(errors)).
Strs("errors", errors).
Msg("Partial success in queue deletion")
}
}
response := StatusResponse{
Status: true,
Error: "", // Could add error details here if needed
}
request.JSONResponse(w, response, http.StatusOK)
}
// handlePause handles pause operations
func (s *SABnzbd) handleQueuePause(w http.ResponseWriter, r *http.Request) {
response := StatusResponse{Status: true}
request.JSONResponse(w, response, http.StatusOK)
}
// handleQueue returns the current download queue
func (s *SABnzbd) handleListQueue(w http.ResponseWriter, r *http.Request) {
nzbs := s.usenet.Store().GetQueue()
queue := Queue{
Version: Version,
Slots: []QueueSlot{},
}
// Convert NZBs to queue slots
for _, nzb := range nzbs {
if nzb.ETA <= 0 {
nzb.ETA = 0 // Ensure ETA is non-negative
}
var timeLeft string
if nzb.ETA == 0 {
timeLeft = "00:00:00" // If ETA is 0, set TimeLeft to "00:00:00"
} else {
// Convert ETA from seconds to "HH:MM:SS" format
duration := time.Duration(nzb.ETA) * time.Second
timeLeft = duration.String()
}
slot := QueueSlot{
Status: s.mapNZBStatus(nzb.Status),
Mb: nzb.TotalSize,
Filename: nzb.Name,
Cat: nzb.Category,
MBLeft: 0,
Percentage: nzb.Percentage,
NzoId: nzb.ID,
Size: nzb.TotalSize,
TimeLeft: timeLeft, // This is in "00:00:00" format
}
queue.Slots = append(queue.Slots, slot)
}
response := QueueResponse{
Queue: queue,
Status: true,
Version: Version,
}
request.JSONResponse(w, response, http.StatusOK)
}
// handleHistory returns the download history
func (s *SABnzbd) handleHistory(w http.ResponseWriter, r *http.Request) {
limitStr := r.FormValue("limit")
if limitStr == "" {
limitStr = "0"
}
limit, err := strconv.Atoi(limitStr)
if err != nil {
s.logger.Error().Err(err).Msg("Invalid limit parameter for history")
s.writeError(w, "Invalid limit parameter", http.StatusBadRequest)
return
}
if limit < 0 {
limit = 0
}
history := s.getHistory(r.Context(), limit)
response := HistoryResponse{
History: history,
}
request.JSONResponse(w, response, http.StatusOK)
}
// handleConfig returns the configuration
func (s *SABnzbd) handleConfig(w http.ResponseWriter, r *http.Request) {
response := ConfigResponse{
Config: s.config,
}
request.JSONResponse(w, response, http.StatusOK)
}
// handleAddURL handles adding NZB by URL
func (s *SABnzbd) handleAddURL(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_arr := getArrFromContext(ctx)
cat := getCategory(ctx)
if _arr == nil {
// If Arr is not in context, create a new one with default values
_arr = arr.New(cat, "", "", false, false, nil, "", "")
}
if r.Method != http.MethodPost {
s.logger.Warn().Str("method", r.Method).Msg("Invalid method")
s.writeError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
url := r.FormValue("name")
action := r.FormValue("action")
downloadDir := r.FormValue("download_dir")
if action == "" {
action = "symlink"
}
if downloadDir == "" {
downloadDir = s.config.Misc.DownloadDir
}
if url == "" {
s.writeError(w, "URL is required", http.StatusBadRequest)
return
}
nzoID, err := s.addNZBURL(ctx, url, _arr, action, downloadDir)
if err != nil {
s.writeError(w, err.Error(), http.StatusInternalServerError)
return
}
if nzoID == "" {
s.writeError(w, "Failed to add NZB", http.StatusInternalServerError)
return
}
response := AddNZBResponse{
Status: true,
NzoIds: []string{nzoID},
}
request.JSONResponse(w, response, http.StatusOK)
}
// handleAddFile handles NZB file uploads
func (s *SABnzbd) handleAddFile(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_arr := getArrFromContext(ctx)
cat := getCategory(ctx)
if _arr == nil {
// If Arr is not in context, create a new one with default values
_arr = arr.New(cat, "", "", false, false, nil, "", "")
}
if r.Method != http.MethodPost {
s.writeError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse multipart form
err := r.ParseMultipartForm(32 << 20) // 32 MB limit
if err != nil {
s.writeError(w, "Failed to parse multipart form", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("name")
if err != nil {
s.writeError(w, "No file uploaded", http.StatusBadRequest)
return
}
defer file.Close()
// Read file content
content, err := io.ReadAll(file)
if err != nil {
s.writeError(w, "Failed to read file", http.StatusInternalServerError)
return
}
action := r.FormValue("action")
downloadDir := r.FormValue("download_dir")
if action == "" {
action = "symlink"
}
if downloadDir == "" {
downloadDir = s.config.Misc.DownloadDir
}
// Process NZB file
nzbID, err := s.addNZBFile(ctx, content, header.Filename, _arr, action, downloadDir)
if err != nil {
s.writeError(w, fmt.Sprintf("Failed to add NZB file: %s", err.Error()), http.StatusInternalServerError)
return
}
if nzbID == "" {
s.writeError(w, "Failed to add NZB file", http.StatusInternalServerError)
return
}
response := AddNZBResponse{
Status: true,
NzoIds: []string{nzbID},
}
request.JSONResponse(w, response, http.StatusOK)
}
// handleVersion returns version information
func (s *SABnzbd) handleVersion(w http.ResponseWriter, r *http.Request) {
response := VersionResponse{
Version: Version,
}
request.JSONResponse(w, response, http.StatusOK)
}
// handleGetCategories returns available categories
func (s *SABnzbd) handleGetCategories(w http.ResponseWriter, r *http.Request) {
categories := s.getCategories()
request.JSONResponse(w, categories, http.StatusOK)
}
// handleGetScripts returns available scripts
func (s *SABnzbd) handleGetScripts(w http.ResponseWriter, r *http.Request) {
scripts := []string{"None"}
request.JSONResponse(w, scripts, http.StatusOK)
}
// handleGetFiles returns files for a specific NZB
func (s *SABnzbd) handleGetFiles(w http.ResponseWriter, r *http.Request) {
nzoID := r.FormValue("value")
var files []string
if nzoID != "" {
nzb := s.usenet.Store().Get(nzoID)
if nzb != nil {
for _, file := range nzb.Files {
files = append(files, file.Name)
}
}
}
request.JSONResponse(w, files, http.StatusOK)
}
func (s *SABnzbd) handleStatus(w http.ResponseWriter, r *http.Request) {
type status struct {
CompletedDir string `json:"completed_dir"`
}
response := struct {
Status status `json:"status"`
}{
Status: status{
CompletedDir: s.config.Misc.DownloadDir,
},
}
request.JSONResponse(w, response, http.StatusOK)
}
// Helper methods
func (s *SABnzbd) getHistory(ctx context.Context, limit int) History {
cat := getCategory(ctx)
items := s.usenet.Store().GetHistory(cat, limit)
slots := make([]HistorySlot, 0, len(items))
history := History{
Version: Version,
Paused: false,
}
for _, item := range items {
slot := HistorySlot{
Status: s.mapNZBStatus(item.Status),
Name: item.Name,
NZBName: item.Name,
NzoId: item.ID,
Category: item.Category,
FailMessage: item.FailMessage,
Bytes: item.TotalSize,
Storage: item.Storage,
}
slots = append(slots, slot)
}
history.Slots = slots
return history
}
func (s *SABnzbd) writeError(w http.ResponseWriter, message string, status int) {
response := StatusResponse{
Status: false,
Error: message,
}
request.JSONResponse(w, response, status)
}
func (s *SABnzbd) mapNZBStatus(status string) string {
switch status {
case "downloading":
return StatusDownloading
case "completed":
return StatusCompleted
case "paused":
return StatusPaused
case "error", "failed":
return StatusFailed
case "processing":
return StatusProcessing
case "verifying":
return StatusVerifying
case "repairing":
return StatusRepairing
case "extracting":
return StatusExtracting
case "moving":
return StatusMoving
case "running":
return StatusRunning
default:
return StatusQueued
}
}
func (s *SABnzbd) addNZBURL(ctx context.Context, url string, arr *arr.Arr, action, downloadDir string) (string, error) {
if url == "" {
return "", fmt.Errorf("URL is required")
}
// Download NZB content
filename, content, err := utils.DownloadFile(url)
if err != nil {
s.logger.Error().Err(err).Str("url", url).Msg("Failed to download NZB from URL")
return "", fmt.Errorf("failed to download NZB from URL: %w", err)
}
if len(content) == 0 {
s.logger.Warn().Str("url", url).Msg("Downloaded content is empty")
return "", fmt.Errorf("downloaded content is empty")
}
return s.addNZBFile(ctx, content, filename, arr, action, downloadDir)
}
func (s *SABnzbd) addNZBFile(ctx context.Context, content []byte, filename string, arr *arr.Arr, action, downloadDir string) (string, error) {
if s.usenet == nil {
return "", fmt.Errorf("store not initialized")
}
req := &usenet.ProcessRequest{
NZBContent: content,
Name: filename,
Arr: arr,
Action: action,
DownloadDir: downloadDir,
}
nzb, err := s.usenet.ProcessNZB(ctx, req)
if err != nil {
return "", fmt.Errorf("failed to process NZB: %w", err)
}
return nzb.ID, nil
}