Implementing a streaming setup with Usenet

This commit is contained in:
Mukhtar Akere
2025-08-01 15:27:24 +01:00
parent afe577bf2f
commit f9861e3b54
65 changed files with 9437 additions and 924 deletions

171
pkg/sabnzbd/config.go Normal file
View File

@@ -0,0 +1,171 @@
package sabnzbd
// ConfigResponse represents configuration response
type ConfigResponse struct {
Config *Config `json:"config"`
}
type ConfigNewzbin struct {
Username string `json:"username"`
BookmarkRate int `json:"bookmark_rate"`
Url string `json:"url"`
Bookmarks int `json:"bookmarks"`
Password string `json:"password"`
Unbookmark int `json:"unbookmark"`
}
// Category represents a SABnzbd category
type Category struct {
Name string `json:"name"`
Order int `json:"order"`
Pp string `json:"pp"`
Script string `json:"script"`
Dir string `json:"dir"`
NewzBin string `json:"newzbin"`
Priority string `json:"priority"`
}
// Server represents a usenet server
type Server struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Connections int `json:"connections"`
Retention int `json:"retention"`
Priority int `json:"priority"`
SSL bool `json:"ssl"`
Optional bool `json:"optional"`
}
type Config struct {
Misc MiscConfig `json:"misc"`
Categories []Category `json:"categories"`
Servers []Server `json:"servers"`
}
type MiscConfig struct {
// Directory Configuration
CompleteDir string `json:"complete_dir"`
DownloadDir string `json:"download_dir"`
AdminDir string `json:"admin_dir"`
NzbBackupDir string `json:"nzb_backup_dir"`
ScriptDir string `json:"script_dir"`
EmailDir string `json:"email_dir"`
WebDir string `json:"web_dir"`
// Processing Options
ParOption string `json:"par_option"`
ParOptionConvert string `json:"par_option_convert"`
ParOptionDuplicate string `json:"par_option_duplicate"`
DirectUnpack string `json:"direct_unpack"`
FlatUnpack string `json:"flat_unpack"`
EnableRecursiveUnpack string `json:"enable_recursive_unpack"`
OverwriteFiles string `json:"overwrite_files"`
IgnoreWrongUnrar string `json:"ignore_wrong_unrar"`
IgnoreUnrarDates string `json:"ignore_unrar_dates"`
PreCheck string `json:"pre_check"`
// File Handling
Permissions string `json:"permissions"`
FolderRename string `json:"folder_rename"`
FileRename string `json:"file_rename"`
ReplaceIllegal string `json:"replace_illegal"`
ReplaceDots string `json:"replace_dots"`
ReplaceSpaces string `json:"replace_spaces"`
SanitizeSafe string `json:"sanitize_safe"`
IgnoreSamples string `json:"ignore_samples"`
UnwantedExtensions []string `json:"unwanted_extensions"`
ActionOnUnwanted string `json:"action_on_unwanted"`
ActionOnDuplicate string `json:"action_on_duplicate"`
BackupForDuplicates string `json:"backup_for_duplicates"`
CleanupList []string `json:"cleanup_list"`
DeobfuscateFinalFilenames string `json:"deobfuscate_final_filenames"`
// Scripts and Processing
PreScript string `json:"pre_script"`
PostScript string `json:"post_script"`
EmptyPostproc string `json:"empty_postproc"`
PauseOnPostProcessing string `json:"pause_on_post_processing"`
// System Resources
Nice string `json:"nice"`
NiceUnpack string `json:"nice_unpack"`
Ionice string `json:"ionice"`
Fsync string `json:"fsync"`
// Bandwidth and Performance
BandwidthMax string `json:"bandwidth_max"`
BandwidthPerc string `json:"bandwidth_perc"`
RefreshRate string `json:"refresh_rate"`
DirscanSpeed string `json:"dirscan_speed"`
FolderMaxLength string `json:"folder_max_length"`
PropagationDelay string `json:"propagation_delay"`
// Storage Management
DownloadFree string `json:"download_free"`
CompleteFree string `json:"complete_free"`
// Queue Management
QueueComplete string `json:"queue_complete"`
QueueCompletePers string `json:"queue_complete_pers"`
AutoSort string `json:"auto_sort"`
NewNzbOnFailure string `json:"new_nzb_on_failure"`
PauseOnPwrar string `json:"pause_on_pwrar"`
WarnedOldQueue string `json:"warned_old_queue"`
// Web Interface
WebHost string `json:"web_host"`
WebPort string `json:"web_port"`
WebUsername string `json:"web_username"`
WebPassword string `json:"web_password"`
WebColor string `json:"web_color"`
WebColor2 string `json:"web_color2"`
AutoBrowser string `json:"auto_browser"`
Autobrowser string `json:"autobrowser"` // Duplicate field - may need to resolve
// HTTPS Configuration
EnableHTTPS string `json:"enable_https"`
EnableHTTPSVerification string `json:"enable_https_verification"`
HTTPSPort string `json:"https_port"`
HTTPSCert string `json:"https_cert"`
HTTPSKey string `json:"https_key"`
HTTPSChain string `json:"https_chain"`
// Security and API
APIKey string `json:"api_key"`
NzbKey string `json:"nzb_key"`
HostWhitelist string `json:"host_whitelist"`
LocalRanges []string `json:"local_ranges"`
InetExposure string `json:"inet_exposure"`
APILogging string `json:"api_logging"`
APIWarnings string `json:"api_warnings"`
// Logging
LogLevel string `json:"log_level"`
LogSize string `json:"log_size"`
MaxLogSize string `json:"max_log_size"`
LogBackups string `json:"log_backups"`
LogNew string `json:"log_new"`
// Notifications
MatrixUsername string `json:"matrix_username"`
MatrixPassword string `json:"matrix_password"`
MatrixServer string `json:"matrix_server"`
MatrixRoom string `json:"matrix_room"`
// Miscellaneous
ConfigLock string `json:"config_lock"`
Language string `json:"language"`
CheckNewRel string `json:"check_new_rel"`
RSSFilenames string `json:"rss_filenames"`
IPv6Hosting string `json:"ipv6_hosting"`
EnableBonjour string `json:"enable_bonjour"`
Cherryhost string `json:"cherryhost"`
WinMenu string `json:"win_menu"`
AMPM string `json:"ampm"`
NotifiedNewSkin string `json:"notified_new_skin"`
HelpURI string `json:"helpuri"`
SSDURI string `json:"ssduri"`
}

121
pkg/sabnzbd/context.go Normal file
View File

@@ -0,0 +1,121 @@
package sabnzbd
import (
"context"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/store"
"net/http"
"strings"
"github.com/sirrobot01/decypharr/pkg/arr"
)
type contextKey string
const (
apiKeyKey contextKey = "apikey"
modeKey contextKey = "mode"
arrKey contextKey = "arr"
categoryKey contextKey = "category"
)
func getMode(ctx context.Context) string {
if mode, ok := ctx.Value(modeKey).(string); ok {
return mode
}
return ""
}
func (s *SABnzbd) categoryContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
category := r.URL.Query().Get("category")
if category == "" {
// Check form data
_ = r.ParseForm()
category = r.Form.Get("category")
}
if category == "" {
category = r.FormValue("category")
}
ctx := context.WithValue(r.Context(), categoryKey, strings.TrimSpace(category))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getArrFromContext(ctx context.Context) *arr.Arr {
if a, ok := ctx.Value(arrKey).(*arr.Arr); ok {
return a
}
return nil
}
func getCategory(ctx context.Context) string {
if category, ok := ctx.Value(categoryKey).(string); ok {
return category
}
return ""
}
// modeContext extracts the mode parameter from the request
func (s *SABnzbd) modeContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mode := r.URL.Query().Get("mode")
if mode == "" {
// Check form data
_ = r.ParseForm()
mode = r.Form.Get("mode")
}
// Extract category for Arr integration
category := r.URL.Query().Get("cat")
if category == "" {
category = r.Form.Get("cat")
}
// Create a default Arr instance for the category
downloadUncached := false
a := arr.New(category, "", "", false, false, &downloadUncached, "", "auto")
ctx := context.WithValue(r.Context(), modeKey, strings.TrimSpace(mode))
ctx = context.WithValue(ctx, arrKey, a)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// authContext creates a middleware that extracts the Arr host and token from the Authorization header
// and adds it to the request context.
// This is used to identify the Arr instance for the request.
// Only a valid host and token will be added to the context/config. The rest are manual
func (s *SABnzbd) authContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.FormValue("ma_username")
token := r.FormValue("ma_password")
category := getCategory(r.Context())
arrs := store.Get().Arr()
// Check if arr exists
a := arrs.Get(category)
if a == nil {
// Arr is not configured, create a new one
downloadUncached := false
a = arr.New(category, "", "", false, false, &downloadUncached, "", "auto")
}
host = strings.TrimSpace(host)
if host != "" {
a.Host = host
}
token = strings.TrimSpace(token)
if token != "" {
a.Token = token
}
a.Source = "auto"
if err := utils.ValidateServiceURL(a.Host); err != nil {
// Return silently, no need to raise a problem. Just do not add the Arr to the context/config.json
next.ServeHTTP(w, r)
return
}
arrs.AddOrUpdate(a)
ctx := context.WithValue(r.Context(), arrKey, a)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

476
pkg/sabnzbd/handlers.go Normal file
View File

@@ -0,0 +1,476 @@
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
}

24
pkg/sabnzbd/routes.go Normal file
View File

@@ -0,0 +1,24 @@
package sabnzbd
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func (s *SABnzbd) Routes() http.Handler {
r := chi.NewRouter()
r.Use(s.categoryContext)
r.Use(s.authContext)
// SABnzbd API endpoints - all under /api with mode parameter
r.Route("/api", func(r chi.Router) {
r.Use(s.modeContext)
// Queue operations
r.Get("/", s.handleAPI)
r.Post("/", s.handleAPI)
})
return r
}

116
pkg/sabnzbd/sabnzbd.go Normal file
View File

@@ -0,0 +1,116 @@
package sabnzbd
import (
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/usenet"
"path/filepath"
)
type SABnzbd struct {
downloadFolder string
config *Config
refreshInterval int
logger zerolog.Logger
usenet usenet.Usenet
defaultCategories []string
}
func New(usenetClient usenet.Usenet) *SABnzbd {
_cfg := config.Get()
cfg := _cfg.SABnzbd
var defaultCategories []string
for _, cat := range _cfg.SABnzbd.Categories {
if cat != "" {
defaultCategories = append(defaultCategories, cat)
}
}
sb := &SABnzbd{
downloadFolder: cfg.DownloadFolder,
refreshInterval: cfg.RefreshInterval,
logger: logger.New("sabnzbd"),
usenet: usenetClient,
defaultCategories: defaultCategories,
}
sb.SetConfig(_cfg)
return sb
}
func (s *SABnzbd) SetConfig(cfg *config.Config) {
sabnzbdConfig := &Config{
Misc: MiscConfig{
CompleteDir: s.downloadFolder,
DownloadDir: s.downloadFolder,
AdminDir: s.downloadFolder,
WebPort: cfg.Port,
Language: "en",
RefreshRate: "1",
QueueComplete: "0",
ConfigLock: "0",
Autobrowser: "1",
CheckNewRel: "1",
},
Categories: s.getCategories(),
}
if cfg.Usenet != nil || len(cfg.Usenet.Providers) == 0 {
for _, provider := range cfg.Usenet.Providers {
if provider.Host == "" || provider.Port == 0 {
continue
}
sabnzbdConfig.Servers = append(sabnzbdConfig.Servers, Server{
Name: provider.Name,
Host: provider.Host,
Port: provider.Port,
Username: provider.Username,
Password: provider.Password,
Connections: provider.Connections,
SSL: provider.SSL,
})
}
}
s.config = sabnzbdConfig
}
func (s *SABnzbd) getCategories() []Category {
_store := store.Get()
arrs := _store.Arr().GetAll()
categories := make([]Category, 0, len(arrs))
added := map[string]struct{}{}
for i, a := range arrs {
if _, ok := added[a.Name]; ok {
continue // Skip if category already added
}
categories = append(categories, Category{
Name: a.Name,
Order: i + 1,
Pp: "3",
Script: "None",
Dir: filepath.Join(s.downloadFolder, a.Name),
Priority: PriorityNormal,
})
}
// Add default categories if not already present
for _, defaultCat := range s.defaultCategories {
if _, ok := added[defaultCat]; ok {
continue // Skip if default category already added
}
categories = append(categories, Category{
Name: defaultCat,
Order: len(categories) + 1,
Pp: "3",
Script: "None",
Dir: filepath.Join(s.downloadFolder, defaultCat),
Priority: PriorityNormal,
})
added[defaultCat] = struct{}{}
}
return categories
}
func (s *SABnzbd) Reset() {
}

150
pkg/sabnzbd/types.go Normal file
View File

@@ -0,0 +1,150 @@
package sabnzbd
// SABnzbd API response types based on official documentation
var (
Version = "4.5.0"
)
// QueueResponse represents the queue status response
type QueueResponse struct {
Queue Queue `json:"queue"`
Status bool `json:"status"`
Version string `json:"version"`
}
// Queue represents the download queue
type Queue struct {
Version string `json:"version"`
Slots []QueueSlot `json:"slots"`
}
// QueueSlot represents a download in the queue
type QueueSlot struct {
Status string `json:"status"`
TimeLeft string `json:"timeleft"`
Mb int64 `json:"mb"`
Filename string `json:"filename"`
Priority string `json:"priority"`
Cat string `json:"cat"`
MBLeft int64 `json:"mbleft"`
Percentage float64 `json:"percentage"`
NzoId string `json:"nzo_id"`
Size int64 `json:"size"`
}
// HistoryResponse represents the history response
type HistoryResponse struct {
History History `json:"history"`
}
// History represents the download history
type History struct {
Version string `json:"version"`
Paused bool `json:"paused"`
Slots []HistorySlot `json:"slots"`
}
// HistorySlot represents a completed download
type HistorySlot struct {
Status string `json:"status"`
Name string `json:"name"`
NZBName string `json:"nzb_name"`
NzoId string `json:"nzo_id"`
Category string `json:"category"`
FailMessage string `json:"fail_message"`
Bytes int64 `json:"bytes"`
Storage string `json:"storage"`
}
// StageLog represents processing stages
type StageLog struct {
Name string `json:"name"`
Actions []string `json:"actions"`
}
// VersionResponse represents version information
type VersionResponse struct {
Version string `json:"version"`
}
// StatusResponse represents general status
type StatusResponse struct {
Status bool `json:"status"`
Error string `json:"error,omitempty"`
}
// FullStatusResponse represents the full status response with queue and history
type FullStatusResponse struct {
Queue Queue `json:"queue"`
History History `json:"history"`
Status bool `json:"status"`
Version string `json:"version"`
}
// AddNZBRequest represents the request to add an NZB
type AddNZBRequest struct {
Name string `json:"name"`
Cat string `json:"cat"`
Script string `json:"script"`
Priority string `json:"priority"`
PP string `json:"pp"`
Password string `json:"password"`
NZBData []byte `json:"nzb_data"`
URL string `json:"url"`
}
// AddNZBResponse represents the response when adding an NZB
type AddNZBResponse struct {
Status bool `json:"status"`
NzoIds []string `json:"nzo_ids"`
Error string `json:"error,omitempty"`
}
// API Mode constants
const (
ModeQueue = "queue"
ModeHistory = "history"
ModeConfig = "config"
ModeGetConfig = "get_config"
ModeAddURL = "addurl"
ModeAddFile = "addfile"
ModeVersion = "version"
ModePause = "pause"
ModeResume = "resume"
ModeDelete = "delete"
ModeShutdown = "shutdown"
ModeRestart = "restart"
ModeGetCats = "get_cats"
ModeGetScripts = "get_scripts"
ModeGetFiles = "get_files"
ModeRetry = "retry"
ModeStatus = "status"
ModeFullStatus = "fullstatus"
)
// Status constants
const (
StatusQueued = "Queued"
StatusPaused = "Paused"
StatusDownloading = "downloading"
StatusProcessing = "Processing"
StatusCompleted = "Completed"
StatusFailed = "Failed"
StatusGrabbing = "Grabbing"
StatusPropagating = "Propagating"
StatusVerifying = "Verifying"
StatusRepairing = "Repairing"
StatusExtracting = "Extracting"
StatusMoving = "Moving"
StatusRunning = "Running"
)
// Priority constants
const (
PriorityForced = "2"
PriorityHigh = "1"
PriorityNormal = "0"
PriorityLow = "-1"
PriorityStop = "-2"
)