Finalize v0.4.0

This commit is contained in:
Mukhtar Akere
2025-01-24 23:33:08 +01:00
parent 66f4965ec8
commit fc5c6e2869
7 changed files with 224 additions and 63 deletions

View File

@@ -272,3 +272,17 @@ func FileReady(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err) // Returns true if the file exists
}
func Remove[S ~[]E, E comparable](s S, values ...E) S {
result := make(S, 0, len(s))
outer:
for _, item := range s {
for _, v := range values {
if item == v {
continue outer
}
}
result = append(result, item)
}
return result
}

View File

@@ -1,6 +1,7 @@
package server
import (
"bytes"
"context"
"encoding/base64"
"github.com/go-chi/chi/v5"
@@ -8,6 +9,7 @@ import (
"github.com/sirrobot01/debrid-blackhole/common"
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
"github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared"
"io"
"net/http"
"path/filepath"
"strings"
@@ -49,12 +51,12 @@ func (q *qbitHandler) CategoryContext(next http.Handler) http.Handler {
category = r.Form.Get("category")
if category == "" {
// Get from multipart form
_ = r.ParseMultipartForm(0)
_ = r.ParseMultipartForm(32 << 20)
category = r.FormValue("category")
}
}
ctx := r.Context()
ctx = context.WithValue(r.Context(), "category", category)
ctx = context.WithValue(r.Context(), "category", strings.TrimSpace(category))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
@@ -67,8 +69,8 @@ func (q *qbitHandler) authContext(next http.Handler) http.Handler {
Name: category,
}
if err == nil {
a.Host = host
a.Token = token
a.Host = strings.TrimSpace(host)
a.Token = strings.TrimSpace(token)
}
q.qbit.Arrs.AddOrUpdate(a)
ctx := context.WithValue(r.Context(), "arr", a)
@@ -88,6 +90,9 @@ func HashesCtx(next http.Handler) http.Handler {
_ = r.ParseForm()
hashes = r.Form["hashes"]
}
for i, hash := range hashes {
hashes[i] = strings.TrimSpace(hash)
}
ctx := context.WithValue(r.Context(), "hashes", hashes)
next.ServeHTTP(w, r.WithContext(ctx))
})
@@ -143,50 +148,49 @@ func (q *qbitHandler) handleTorrentsInfo(w http.ResponseWriter, r *http.Request)
func (q *qbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0]
switch contentType {
case "multipart/form-data":
err := r.ParseMultipartForm(32 << 20) // 32MB max memory
if err != nil {
q.logger.Info().Msgf("Error parsing form: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
case "application/x-www-form-urlencoded":
err := r.ParseForm()
if err != nil {
body, _ := io.ReadAll(r.Body)
q.logger.Debug().Msgf("Raw request body: %s", string(body))
r.Body = io.NopCloser(bytes.NewBuffer(body))
// Parse form based on content type
contentType := r.Header.Get("Content-Type")
if strings.Contains(contentType, "multipart/form-data") {
if err := r.ParseMultipartForm(32 << 20); err != nil {
q.logger.Info().Msgf("Error parsing multipart form: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else if strings.Contains(contentType, "application/x-www-form-urlencoded") {
if err := r.ParseForm(); err != nil {
q.logger.Info().Msgf("Error parsing form: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else {
http.Error(w, "Invalid content type", http.StatusBadRequest)
return
}
q.logger.Debug().Msgf("All form values: %+v", r.Form)
q.logger.Debug().Msgf("URLs value: %q", r.FormValue("urls"))
q.logger.Debug().Msgf("Content-Type: %s", r.Header.Get("Content-Type"))
isSymlink := strings.ToLower(r.FormValue("sequentialDownload")) != "true"
q.logger.Info().Msgf("isSymlink: %v", isSymlink)
urls := r.FormValue("urls")
category := r.FormValue("category")
atleastOne := false
var urlList []string
if urls != "" {
urlList = strings.Split(urls, "\n")
}
ctx = context.WithValue(ctx, "isSymlink", isSymlink)
for _, url := range urlList {
if err := q.qbit.AddMagnet(ctx, url, category); err != nil {
q.logger.Info().Msgf("Error adding magnet: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
// Handle magnet URLs
if urls := r.FormValue("urls"); urls != "" {
var urlList []string
for _, u := range strings.Split(urls, "\n") {
urlList = append(urlList, strings.TrimSpace(u))
}
atleastOne = true
}
if contentType == "multipart/form-data" && len(r.MultipartForm.File["torrents"]) > 0 {
files := r.MultipartForm.File["torrents"]
for _, fileHeader := range files {
if err := q.qbit.AddTorrent(ctx, fileHeader, category); err != nil {
q.logger.Info().Msgf("Error adding torrent: %v", err)
ctx = context.WithValue(ctx, "isSymlink", isSymlink)
for _, url := range urlList {
if err := q.qbit.AddMagnet(ctx, url, category); err != nil {
q.logger.Info().Msgf("Error adding magnet: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -194,6 +198,20 @@ func (q *qbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request)
}
}
// Handle torrent files
if r.MultipartForm != nil && r.MultipartForm.File != nil {
if files := r.MultipartForm.File["torrents"]; len(files) > 0 {
for _, fileHeader := range files {
if err := q.qbit.AddTorrent(ctx, fileHeader, category); err != nil {
q.logger.Info().Msgf("Error adding torrent: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
atleastOne = true
}
}
}
if !atleastOne {
http.Error(w, "No valid URLs or torrents provided", http.StatusBadRequest)
return
@@ -304,3 +322,72 @@ func (q *qbitHandler) handleTorrentFiles(w http.ResponseWriter, r *http.Request)
files := q.qbit.GetTorrentFiles(torrent)
common.JSONResponse(w, files, http.StatusOK)
}
func (q *qbitHandler) handleSetCategory(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
category := ctx.Value("category").(string)
hashes, _ := ctx.Value("hashes").([]string)
torrents := q.qbit.Storage.GetAll("", "", hashes)
for _, torrent := range torrents {
torrent.Category = category
q.qbit.Storage.AddOrUpdate(torrent)
}
common.JSONResponse(w, nil, http.StatusOK)
}
func (q *qbitHandler) handleAddTorrentTags(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
tags := strings.Split(r.FormValue("tags"), ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
torrents := q.qbit.Storage.GetAll("", "", hashes)
for _, t := range torrents {
q.qbit.SetTorrentTags(t, tags)
}
common.JSONResponse(w, nil, http.StatusOK)
}
func (q *qbitHandler) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
tags := strings.Split(r.FormValue("tags"), ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
torrents := q.qbit.Storage.GetAll("", "", hashes)
for _, torrent := range torrents {
q.qbit.RemoveTorrentTags(torrent, tags)
}
common.JSONResponse(w, nil, http.StatusOK)
}
func (q *qbitHandler) handleGetTags(w http.ResponseWriter, r *http.Request) {
common.JSONResponse(w, q.qbit.Tags, http.StatusOK)
}
func (q *qbitHandler) handleCreateTags(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
tags := strings.Split(r.FormValue("tags"), ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
q.qbit.AddTags(tags)
common.JSONResponse(w, nil, http.StatusOK)
}

View File

@@ -22,7 +22,11 @@ func (q *qbitHandler) Routes(r chi.Router) http.Handler {
r.Post("/delete", q.handleTorrentsDelete)
r.Get("/categories", q.handleCategories)
r.Post("/createCategory", q.handleCreateCategory)
r.Post("/setCategory", q.handleSetCategory)
r.Post("/addTags", q.handleAddTorrentTags)
r.Post("/removeTags", q.handleRemoveTorrentTags)
r.Post("/createTags", q.handleCreateTags)
r.Get("/tags", q.handleGetTags)
r.Get("/pause", q.handleTorrentsPause)
r.Get("/resume", q.handleTorrentsResume)
r.Get("/recheck", q.handleTorrentRecheck)
@@ -42,24 +46,3 @@ func (q *qbitHandler) Routes(r chi.Router) http.Handler {
})
return r
}
func (u *uiHandler) Routes(r chi.Router) http.Handler {
r.Group(func(r chi.Router) {
r.Get("/", u.IndexHandler)
r.Get("/download", u.DownloadHandler)
r.Get("/repair", u.RepairHandler)
r.Get("/config", u.ConfigHandler)
r.Route("/internal", func(r chi.Router) {
r.Get("/arrs", u.handleGetArrs)
r.Post("/add", u.handleAddContent)
r.Get("/cached", u.handleCheckCached)
r.Post("/repair", u.handleRepairMedia)
r.Get("/torrents", u.handleGetTorrents)
r.Delete("/torrents/{hash}", u.handleDeleteTorrent)
r.Get("/config", u.handleGetConfig)
r.Get("/version", u.handleGetVersion)
})
})
return r
}

View File

@@ -51,7 +51,7 @@
</td>
<td>${formatSpeed(torrent.dlspeed)}</td>
<td><span class="badge bg-secondary">${torrent.category || 'None'}</span></td>
<td><span class="badge bg-secondary">${torrent.debrid || 'None'}</span></td>
<td>${torrent.debrid || 'None'}</td>
<td><span class="badge ${getStateColor(torrent.state)}">${torrent.state}</span></td>
<td>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}')">
@@ -93,6 +93,11 @@
tbody.innerHTML = torrents.map(torrent => torrentRowTemplate(torrent)).join('');
// Update category filter options
let category = document.getElementById('categoryFilter').value;
document.querySelectorAll('#torrentsList tr').forEach(row => {
const rowCategory = row.querySelector('td:nth-child(5)').textContent;
row.style.display = (!category || rowCategory.includes(category)) ? '' : 'none';
});
updateCategoryFilter(torrents);
} catch (error) {
console.error('Error loading torrents:', error);

View File

@@ -0,0 +1,27 @@
package server
import (
"github.com/go-chi/chi/v5"
"net/http"
)
func (u *uiHandler) Routes(r chi.Router) http.Handler {
r.Group(func(r chi.Router) {
r.Get("/", u.IndexHandler)
r.Get("/download", u.DownloadHandler)
r.Get("/repair", u.RepairHandler)
r.Get("/config", u.ConfigHandler)
r.Route("/internal", func(r chi.Router) {
r.Get("/arrs", u.handleGetArrs)
r.Post("/add", u.handleAddContent)
r.Get("/cached", u.handleCheckCached)
r.Post("/repair", u.handleRepairMedia)
r.Get("/torrents", u.handleGetTorrents)
r.Delete("/torrents/{hash}", u.handleDeleteTorrent)
r.Get("/config", u.handleGetConfig)
r.Get("/version", u.handleGetVersion)
})
})
return r
}

View File

@@ -20,6 +20,7 @@ type QBit struct {
debug bool
logger zerolog.Logger
Arrs *arr.Storage
Tags []string
RefreshInterval int
}

View File

@@ -12,6 +12,7 @@ import (
"mime/multipart"
"os"
"path/filepath"
"slices"
"strings"
"time"
)
@@ -266,12 +267,55 @@ func (q *QBit) GetTorrentFiles(t *Torrent) []*TorrentFile {
if t.DebridTorrent == nil {
return files
}
for index, file := range t.DebridTorrent.Files {
for _, file := range t.DebridTorrent.Files {
files = append(files, &TorrentFile{
Index: index,
Name: file.Path,
Size: file.Size,
Name: file.Path,
Size: file.Size,
})
}
return files
}
func (q *QBit) SetTorrentTags(t *Torrent, tags []string) bool {
torrentTags := strings.Split(t.Tags, ",")
for _, tag := range tags {
if tag == "" {
continue
}
if !slices.Contains(torrentTags, tag) {
torrentTags = append(torrentTags, tag)
}
if !slices.Contains(q.Tags, tag) {
q.Tags = append(q.Tags, tag)
}
}
t.Tags = strings.Join(torrentTags, ",")
q.Storage.Update(t)
return true
}
func (q *QBit) RemoveTorrentTags(t *Torrent, tags []string) bool {
torrentTags := strings.Split(t.Tags, ",")
newTorrentTags := common.Remove(torrentTags, tags...)
q.Tags = common.Remove(q.Tags, tags...)
t.Tags = strings.Join(newTorrentTags, ",")
q.Storage.Update(t)
return true
}
func (q *QBit) AddTags(tags []string) bool {
for _, tag := range tags {
if tag == "" {
continue
}
if !slices.Contains(q.Tags, tag) {
q.Tags = append(q.Tags, tag)
}
}
return true
}
func (q *QBit) RemoveTags(tags []string) bool {
q.Tags = common.Remove(q.Tags, tags...)
return true
}