From 9de3187e416dc2a05857d746e7e35a5135e68306 Mon Sep 17 00:00:00 2001 From: "Calmcacil B." <716671+calmcacil@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:42:32 +0200 Subject: [PATCH 1/3] fix: support URL-encoded form data in qBittorrent add endpoint Sonarr/Radarr send URL-encoded form data (not multipart) when adding magnet links. Check Content-Type and fall back to ParseForm when the request is not multipart, instead of unconditionally calling ParseMultipartForm. --- internal/api/qbit.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/api/qbit.go b/internal/api/qbit.go index 3a5f8fc..8644181 100644 --- a/internal/api/qbit.go +++ b/internal/api/qbit.go @@ -113,9 +113,17 @@ func (s *Server) handleQBitTransferInfo(w http.ResponseWriter, r *http.Request) } func (s *Server) handleQBitAdd(w http.ResponseWriter, r *http.Request) { - if err := r.ParseMultipartForm(8 << 20); err != nil { // 8 MB; torrent files are typically < 1 MB - http.Error(w, err.Error(), http.StatusBadRequest) - return + contentType := r.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "multipart/form-data") { + if err := r.ParseMultipartForm(8 << 20); err != nil { // 8 MB; torrent files are typically < 1 MB + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } else { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } } category := strings.TrimSpace(r.PostFormValue("category")) From 528888256fe9d42b2c9032465f22f50782d88a60 Mon Sep 17 00:00:00 2001 From: "Calmcacil B." <716671+calmcacil@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:59:42 +0200 Subject: [PATCH 2/3] fix: parse Content-Type with mime.ParseMediaType for robust multipart detection Covers uppercase Multipart/Form-Data and extra params (e.g. boundary), per upstream review feedback on PR #2. --- internal/api/qbit.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/api/qbit.go b/internal/api/qbit.go index 8644181..19ab21b 100644 --- a/internal/api/qbit.go +++ b/internal/api/qbit.go @@ -1,6 +1,7 @@ package api import ( + "mime" "net/http" "net/url" "os" @@ -114,7 +115,8 @@ func (s *Server) handleQBitTransferInfo(w http.ResponseWriter, r *http.Request) func (s *Server) handleQBitAdd(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") - if strings.HasPrefix(contentType, "multipart/form-data") { + mediaType, _, _ := mime.ParseMediaType(contentType) + if mediaType == "multipart/form-data" { if err := r.ParseMultipartForm(8 << 20); err != nil { // 8 MB; torrent files are typically < 1 MB http.Error(w, err.Error(), http.StatusBadRequest) return From 07386b6db63c73507a62b3cb84630de16060b0b3 Mon Sep 17 00:00:00 2001 From: "Calmcacil B." <716671+calmcacil@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:50:17 +0200 Subject: [PATCH 3/3] fix: report ratio_limit 0 so Sonarr reaches seed limit and removes torrent Sonarr's HasReachedSeedLimit returns false when the torrent has no seed limit set, so for a debrid download (ratio 0, never seeds) it never marks the torrent as removable and copies instead of moving, then leaves it in the client forever. Reporting ratio_limit 0 with ratio 0 satisfies the first branch of HasReachedSeedLimit (0 - 0 <= 0.001) so Sonarr treats the completed torrent as past its seed goal and sends torrents/delete after import. Keeps state as pausedUP (which Sonarr recognizes) and adds the seeding_time field for completeness. --- internal/compat/qbit.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/compat/qbit.go b/internal/compat/qbit.go index c544b08..397dc7e 100644 --- a/internal/compat/qbit.go +++ b/internal/compat/qbit.go @@ -50,6 +50,8 @@ type QBitTorrentInfo struct { Priority int `json:"priority"` Progress float64 `json:"progress"` Ratio float64 `json:"ratio"` + RatioLimit float64 `json:"ratio_limit"` + SeedingTime int64 `json:"seeding_time"` SavePath string `json:"save_path"` Size int64 `json:"size"` State string `json:"state"` @@ -120,6 +122,8 @@ func ProjectQBitTorrent(job *store.Job) QBitTorrentInfo { Priority: 0, Progress: progress, Ratio: 0, + RatioLimit: 0, + SeedingTime: 0, SavePath: savePath, Size: max(total, done), State: state,