From 2fa6737f3171a34c04cfd7710853b07d17bf6d1a Mon Sep 17 00:00:00 2001 From: Elias Benbourenane Date: Fri, 31 Jan 2025 21:41:00 -0500 Subject: [PATCH] Ability to upload torrent files (#24) - Upload Torrent file OR magnet URI --- common/utils.go | 64 +++++++++++------------- pkg/qbit/server/templates/download.html | 52 ++++++++++++------- pkg/qbit/server/ui_handlers.go | 66 ++++++++++++++++++------- 3 files changed, 112 insertions(+), 70 deletions(-) diff --git a/common/utils.go b/common/utils.go index 0732ae1..e458edd 100644 --- a/common/utils.go +++ b/common/utils.go @@ -2,11 +2,11 @@ package common import ( "bufio" + "bytes" "context" "encoding/base32" "encoding/hex" "fmt" - "github.com/anacrolix/torrent/metainfo" "io" "log" "math/rand" @@ -17,6 +17,8 @@ import ( "regexp" "strings" "time" + + "github.com/anacrolix/torrent/metainfo" ) type Magnet struct { @@ -28,23 +30,11 @@ type Magnet struct { func GetMagnetFromFile(file io.Reader, filePath string) (*Magnet, error) { if filepath.Ext(filePath) == ".torrent" { - mi, err := metainfo.Load(file) + torrentData, err := io.ReadAll(file) if err != nil { return nil, err } - hash := mi.HashInfoBytes() - infoHash := hash.HexString() - info, err := mi.UnmarshalInfo() - if err != nil { - return nil, err - } - magnet := &Magnet{ - InfoHash: infoHash, - Name: info.Name, - Size: info.Length, - Link: mi.Magnet(&hash, &info).String(), - } - return magnet, nil + return GetMagnetFromBytes(torrentData) } else { // .magnet file magnetLink := ReadMagnetFile(file) @@ -61,6 +51,28 @@ func GetMagnetFromUrl(url string) (*Magnet, error) { return nil, fmt.Errorf("invalid url") } +func GetMagnetFromBytes(torrentData []byte) (*Magnet, error) { + // Create a scanner to read the file line by line + mi, err := metainfo.Load(bytes.NewReader(torrentData)) + if err != nil { + return nil, err + } + hash := mi.HashInfoBytes() + infoHash := hash.HexString() + info, err := mi.UnmarshalInfo() + if err != nil { + return nil, err + } + log.Println("InfoHash: ", infoHash) + magnet := &Magnet{ + InfoHash: infoHash, + Name: info.Name, + Size: info.Length, + Link: mi.Magnet(&hash, &info).String(), + } + return magnet, nil +} + func OpenMagnetFile(filePath string) string { file, err := os.Open(filePath) if err != nil { @@ -103,27 +115,11 @@ func OpenMagnetHttpURL(magnetLink string) (*Magnet, error) { return } }(resp) // Ensure the response is closed after the function ends - - // Create a scanner to read the file line by line - - mi, err := metainfo.Load(resp.Body) + torrentData, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, fmt.Errorf("error reading response body: %v", err) } - hash := mi.HashInfoBytes() - infoHash := hash.HexString() - info, err := mi.UnmarshalInfo() - if err != nil { - return nil, err - } - log.Println("InfoHash: ", infoHash) - magnet := &Magnet{ - InfoHash: infoHash, - Name: info.Name, - Size: info.Length, - Link: mi.Magnet(&hash, &info).String(), - } - return magnet, nil + return GetMagnetFromBytes(torrentData) } func GetMagnetInfo(magnetLink string) (*Magnet, error) { diff --git a/pkg/qbit/server/templates/download.html b/pkg/qbit/server/templates/download.html index bd47624..4e87007 100644 --- a/pkg/qbit/server/templates/download.html +++ b/pkg/qbit/server/templates/download.html @@ -5,20 +5,26 @@

Add New Download

-
-
- - + +
+ +
+
+ +
+ +
+
- +
- + @@ -62,31 +68,40 @@ submitBtn.innerHTML = 'Adding...'; try { + const formData = new FormData(); + + // Add URLs if present const urls = document.getElementById('magnetURI').value .split('\n') .map(url => url.trim()) .filter(url => url.length > 0); - if (urls.length === 0) { + if (urls.length > 0) { + formData.append('urls', urls.join('\n')); + } + + // Add torrent files if present + const fileInput = document.getElementById('torrentFiles'); + for (let i = 0; i < fileInput.files.length; i++) { + formData.append('files', fileInput.files[i]); + } + + if (urls.length + fileInput.files.length === 0) { createToast('Please submit at least one torrent', 'warning'); return; } - if (urls.length >= 100) { - createToast('Please submit less than 100 torrents at a time', 'warning'); + + if (urls.length + fileInput.files.length > 100) { + createToast('Please submit up to 100 torrents at a time', 'warning'); return; } + formData.append('arr', document.getElementById('category').value); + formData.append('notSymlink', document.getElementById('isSymlink').checked); const response = await fetch('/internal/add', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - urls: urls, - arr: document.getElementById('category').value, - notSymlink: document.getElementById('isSymlink').checked - }) + body: formData }); const result = await response.json(); @@ -100,6 +115,9 @@ } else { createToast(`Successfully added ${result.results.length} torrents!`); } + + document.getElementById('magnetURI').value = ''; + document.getElementById('torrentFiles').value = ''; } catch (error) { createToast(`Error adding downloads: ${error.message}`, 'error'); } finally { diff --git a/pkg/qbit/server/ui_handlers.go b/pkg/qbit/server/ui_handlers.go index a1f1a99..ba27986 100644 --- a/pkg/qbit/server/ui_handlers.go +++ b/pkg/qbit/server/ui_handlers.go @@ -117,37 +117,65 @@ func (u *uiHandler) handleGetArrs(w http.ResponseWriter, r *http.Request) { } func (u *uiHandler) handleAddContent(w http.ResponseWriter, r *http.Request) { - var req struct { - URLs []string `json:"urls"` - Arr string `json:"arr"` - NotSymlink bool `json:"notSymlink"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := r.ParseMultipartForm(32 << 20); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - results := make([]*ImportRequest, 0, len(req.URLs)) + results := make([]*ImportRequest, 0) errs := make([]string, 0) - _arr := u.qbit.Arrs.Get(req.Arr) + arrName := r.FormValue("arr") + notSymlink := r.FormValue("notSymlink") == "true" + + _arr := u.qbit.Arrs.Get(arrName) if _arr == nil { - _arr = arr.NewArr(req.Arr, "", "", arr.Sonarr) + _arr = arr.NewArr(arrName, "", "", arr.Sonarr) } - for _, url := range req.URLs { - if url == "" { - continue + // Handle URLs + if urls := r.FormValue("urls"); urls != "" { + var urlList []string + for _, u := range strings.Split(urls, "\n") { + if trimmed := strings.TrimSpace(u); trimmed != "" { + urlList = append(urlList, trimmed) + } } - importReq := NewImportRequest(url, _arr, !req.NotSymlink) - err := importReq.Process(u.qbit) - if err != nil { - errs = append(errs, fmt.Sprintf("URL %s: %v", url, err)) - continue + for _, url := range urlList { + importReq := NewImportRequest(url, _arr, !notSymlink) + err := importReq.Process(u.qbit) + if err != nil { + errs = append(errs, fmt.Sprintf("URL %s: %v", url, err)) + continue + } + results = append(results, importReq) + } + } + + // Handle torrent/magnet files + if files := r.MultipartForm.File["files"]; len(files) > 0 { + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + errs = append(errs, fmt.Sprintf("Failed to open file %s: %v", fileHeader.Filename, err)) + continue + } + + magnet, err := common.GetMagnetFromFile(file, fileHeader.Filename) + if err != nil { + errs = append(errs, fmt.Sprintf("Failed to parse torrent file %s: %v", fileHeader.Filename, err)) + continue + } + + importReq := NewImportRequest(magnet.Link, _arr, !notSymlink) + err = importReq.Process(u.qbit) + if err != nil { + errs = append(errs, fmt.Sprintf("File %s: %v", fileHeader.Filename, err)) + continue + } + results = append(results, importReq) } - results = append(results, importReq) } common.JSONResponse(w, struct {