Finalize v0.4.0
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
27
pkg/qbit/server/ui_routes.go
Normal file
27
pkg/qbit/server/ui_routes.go
Normal 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
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type QBit struct {
|
||||
debug bool
|
||||
logger zerolog.Logger
|
||||
Arrs *arr.Storage
|
||||
Tags []string
|
||||
RefreshInterval int
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user