477 lines
12 KiB
Go
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
|
|
}
|