Add support for same infohashes but different categories

This commit is contained in:
Mukhtar Akere
2025-02-22 00:14:13 +01:00
parent 108da305b3
commit 7af0de76cc
7 changed files with 113 additions and 66 deletions
+13 -6
View File
@@ -213,8 +213,9 @@ func (q *QBit) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) {
http.Error(w, "No hashes provided", http.StatusBadRequest) http.Error(w, "No hashes provided", http.StatusBadRequest)
return return
} }
category := ctx.Value("category").(string)
for _, hash := range hashes { for _, hash := range hashes {
q.Storage.Delete(hash) q.Storage.Delete(hash, category)
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@@ -223,8 +224,9 @@ func (q *QBit) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) {
func (q *QBit) handleTorrentsPause(w http.ResponseWriter, r *http.Request) { func (q *QBit) handleTorrentsPause(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string) hashes, _ := ctx.Value("hashes").([]string)
category := ctx.Value("category").(string)
for _, hash := range hashes { for _, hash := range hashes {
torrent := q.Storage.Get(hash) torrent := q.Storage.Get(hash, category)
if torrent == nil { if torrent == nil {
continue continue
} }
@@ -237,8 +239,9 @@ func (q *QBit) handleTorrentsPause(w http.ResponseWriter, r *http.Request) {
func (q *QBit) handleTorrentsResume(w http.ResponseWriter, r *http.Request) { func (q *QBit) handleTorrentsResume(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string) hashes, _ := ctx.Value("hashes").([]string)
category := ctx.Value("category").(string)
for _, hash := range hashes { for _, hash := range hashes {
torrent := q.Storage.Get(hash) torrent := q.Storage.Get(hash, category)
if torrent == nil { if torrent == nil {
continue continue
} }
@@ -251,8 +254,9 @@ func (q *QBit) handleTorrentsResume(w http.ResponseWriter, r *http.Request) {
func (q *QBit) handleTorrentRecheck(w http.ResponseWriter, r *http.Request) { func (q *QBit) handleTorrentRecheck(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string) hashes, _ := ctx.Value("hashes").([]string)
category := ctx.Value("category").(string)
for _, hash := range hashes { for _, hash := range hashes {
torrent := q.Storage.Get(hash) torrent := q.Storage.Get(hash, category)
if torrent == nil { if torrent == nil {
continue continue
} }
@@ -293,15 +297,18 @@ func (q *QBit) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
} }
func (q *QBit) handleTorrentProperties(w http.ResponseWriter, r *http.Request) { func (q *QBit) handleTorrentProperties(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hash := r.URL.Query().Get("hash") hash := r.URL.Query().Get("hash")
torrent := q.Storage.Get(hash) torrent := q.Storage.Get(hash, ctx.Value("category").(string))
properties := q.GetTorrentProperties(torrent) properties := q.GetTorrentProperties(torrent)
request.JSONResponse(w, properties, http.StatusOK) request.JSONResponse(w, properties, http.StatusOK)
} }
func (q *QBit) handleTorrentFiles(w http.ResponseWriter, r *http.Request) { func (q *QBit) handleTorrentFiles(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hash := r.URL.Query().Get("hash") hash := r.URL.Query().Get("hash")
torrent := q.Storage.Get(hash) torrent := q.Storage.Get(hash, ctx.Value("category").(string))
if torrent == nil { if torrent == nil {
return return
} }
+59 -44
View File
@@ -2,23 +2,32 @@ package qbit
import ( import (
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"sync" "sync"
) )
func keyPair(hash, category string) string {
if category == "" {
category = "uncategorized"
}
return fmt.Sprintf("%s|%s", hash, category)
}
type Torrents = map[string]*Torrent
type TorrentStorage struct { type TorrentStorage struct {
torrents map[string]*Torrent torrents Torrents
mu sync.RWMutex mu sync.RWMutex
order []string
filename string // Added to store the filename for persistence filename string // Added to store the filename for persistence
} }
func loadTorrentsFromJSON(filename string) (map[string]*Torrent, error) { func loadTorrentsFromJSON(filename string) (Torrents, error) {
data, err := os.ReadFile(filename) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
torrents := make(map[string]*Torrent) torrents := make(Torrents)
if err := json.Unmarshal(data, &torrents); err != nil { if err := json.Unmarshal(data, &torrents); err != nil {
return nil, err return nil, err
} }
@@ -29,16 +38,11 @@ func NewTorrentStorage(filename string) *TorrentStorage {
// Open the JSON file and read the data // Open the JSON file and read the data
torrents, err := loadTorrentsFromJSON(filename) torrents, err := loadTorrentsFromJSON(filename)
if err != nil { if err != nil {
torrents = make(map[string]*Torrent) torrents = make(Torrents)
}
order := make([]string, 0, len(torrents))
for id := range torrents {
order = append(order, id)
} }
// Create a new TorrentStorage // Create a new TorrentStorage
return &TorrentStorage{ return &TorrentStorage{
torrents: torrents, torrents: torrents,
order: order,
filename: filename, filename: filename,
} }
} }
@@ -46,44 +50,37 @@ func NewTorrentStorage(filename string) *TorrentStorage {
func (ts *TorrentStorage) Add(torrent *Torrent) { func (ts *TorrentStorage) Add(torrent *Torrent) {
ts.mu.Lock() ts.mu.Lock()
defer ts.mu.Unlock() defer ts.mu.Unlock()
ts.torrents[torrent.Hash] = torrent ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent
ts.order = append(ts.order, torrent.Hash)
_ = ts.saveToFile() _ = ts.saveToFile()
} }
func (ts *TorrentStorage) AddOrUpdate(torrent *Torrent) { func (ts *TorrentStorage) AddOrUpdate(torrent *Torrent) {
ts.mu.Lock() ts.mu.Lock()
defer ts.mu.Unlock() defer ts.mu.Unlock()
if _, exists := ts.torrents[torrent.Hash]; !exists { ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent
ts.order = append(ts.order, torrent.Hash)
}
ts.torrents[torrent.Hash] = torrent
_ = ts.saveToFile() _ = ts.saveToFile()
} }
func (ts *TorrentStorage) GetByID(id string) *Torrent { func (ts *TorrentStorage) Get(hash, category string) *Torrent {
ts.mu.RLock() ts.mu.RLock()
defer ts.mu.RUnlock() defer ts.mu.RUnlock()
for _, torrent := range ts.torrents { torrent, exists := ts.torrents[keyPair(hash, category)]
if torrent.ID == id { if !exists && category == "" {
return torrent // Try to find the torrent without knowing the category
for _, t := range ts.torrents {
if t.Hash == hash {
return t
}
} }
} }
return nil return torrent
}
func (ts *TorrentStorage) Get(hash string) *Torrent {
ts.mu.RLock()
defer ts.mu.RUnlock()
return ts.torrents[hash]
} }
func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string) []*Torrent { func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string) []*Torrent {
ts.mu.RLock() ts.mu.RLock()
defer ts.mu.RUnlock() defer ts.mu.RUnlock()
torrents := make([]*Torrent, 0) torrents := make([]*Torrent, 0)
for _, id := range ts.order { for _, torrent := range ts.torrents {
torrent := ts.torrents[id]
if category != "" && torrent.Category != category { if category != "" && torrent.Category != category {
continue continue
} }
@@ -92,14 +89,17 @@ func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string
} }
torrents = append(torrents, torrent) torrents = append(torrents, torrent)
} }
if len(hashes) > 0 { if len(hashes) > 0 {
filtered := make([]*Torrent, 0, len(torrents)) filtered := make([]*Torrent, 0)
for _, hash := range hashes { for _, hash := range hashes {
if torrent := ts.torrents[hash]; torrent != nil { for _, torrent := range torrents {
filtered = append(filtered, torrent) if torrent.Hash == hash {
filtered = append(filtered, torrent)
}
} }
} }
torrents = filtered return filtered
} }
return torrents return torrents
} }
@@ -107,24 +107,26 @@ func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string
func (ts *TorrentStorage) Update(torrent *Torrent) { func (ts *TorrentStorage) Update(torrent *Torrent) {
ts.mu.Lock() ts.mu.Lock()
defer ts.mu.Unlock() defer ts.mu.Unlock()
ts.torrents[torrent.Hash] = torrent ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent
_ = ts.saveToFile() _ = ts.saveToFile()
} }
func (ts *TorrentStorage) Delete(hash string) { func (ts *TorrentStorage) Delete(hash, category string) {
ts.mu.Lock() ts.mu.Lock()
defer ts.mu.Unlock() defer ts.mu.Unlock()
torrent, exists := ts.torrents[hash] key := keyPair(hash, category)
if !exists { torrent, exists := ts.torrents[key]
return if !exists && category == "" {
} // Remove the torrent without knowing the category
delete(ts.torrents, hash) for k, t := range ts.torrents {
for i, id := range ts.order { if t.Hash == hash {
if id == hash { key = k
ts.order = append(ts.order[:i], ts.order[i+1:]...) torrent = t
break break
}
} }
} }
delete(ts.torrents, key)
// Delete the torrent folder // Delete the torrent folder
if torrent.ContentPath != "" { if torrent.ContentPath != "" {
err := os.RemoveAll(torrent.ContentPath) err := os.RemoveAll(torrent.ContentPath)
@@ -135,6 +137,19 @@ func (ts *TorrentStorage) Delete(hash string) {
_ = ts.saveToFile() _ = ts.saveToFile()
} }
func (ts *TorrentStorage) DeleteMultiple(hashes []string) {
ts.mu.Lock()
defer ts.mu.Unlock()
for _, hash := range hashes {
for key, torrent := range ts.torrents {
if torrent.Hash == hash {
delete(ts.torrents, key)
}
}
}
_ = ts.saveToFile()
}
func (ts *TorrentStorage) Save() error { func (ts *TorrentStorage) Save() error {
ts.mu.RLock() ts.mu.RLock()
defer ts.mu.RUnlock() defer ts.mu.RUnlock()
+1 -1
View File
@@ -172,7 +172,7 @@ type TorrentCategory struct {
} }
type Torrent struct { type Torrent struct {
ID string `json:"-"` ID string `json:"id"`
DebridTorrent *torrent.Torrent `json:"-"` DebridTorrent *torrent.Torrent `json:"-"`
Debrid string `json:"debrid"` Debrid string `json:"debrid"`
TorrentPath string `json:"-"` TorrentPath string `json:"-"`
+2 -1
View File
@@ -24,7 +24,8 @@ func (ui *Handler) Routes() http.Handler {
r.Post("/add", ui.handleAddContent) r.Post("/add", ui.handleAddContent)
r.Post("/repair", ui.handleRepairMedia) r.Post("/repair", ui.handleRepairMedia)
r.Get("/torrents", ui.handleGetTorrents) r.Get("/torrents", ui.handleGetTorrents)
r.Delete("/torrents/{hash}", ui.handleDeleteTorrent) r.Delete("/torrents/{category}/{hash}", ui.handleDeleteTorrent)
r.Delete("/torrents/", ui.handleDeleteTorrents)
r.Get("/config", ui.handleGetConfig) r.Get("/config", ui.handleGetConfig)
r.Get("/version", ui.handleGetVersion) r.Get("/version", ui.handleGetVersion)
}) })
+13 -1
View File
@@ -411,11 +411,23 @@ func (ui *Handler) handleGetTorrents(w http.ResponseWriter, r *http.Request) {
func (ui *Handler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) { func (ui *Handler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) {
hash := chi.URLParam(r, "hash") hash := chi.URLParam(r, "hash")
category := r.URL.Query().Get("category")
if hash == "" { if hash == "" {
http.Error(w, "No hash provided", http.StatusBadRequest) http.Error(w, "No hash provided", http.StatusBadRequest)
return return
} }
ui.qbit.Storage.Delete(hash) ui.qbit.Storage.Delete(hash, category)
w.WriteHeader(http.StatusOK)
}
func (ui *Handler) handleDeleteTorrents(w http.ResponseWriter, r *http.Request) {
hashesStr := r.URL.Query().Get("hashes")
if hashesStr == "" {
http.Error(w, "No hashes provided", http.StatusBadRequest)
return
}
hashes := strings.Split(hashesStr, ",")
ui.qbit.Storage.DeleteMultiple(hashes)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
+8 -7
View File
@@ -86,7 +86,7 @@
<td>${torrent.debrid || 'None'}</td> <td>${torrent.debrid || 'None'}</td>
<td><span class="badge ${getStateColor(torrent.state)}">${torrent.state}</span></td> <td><span class="badge ${getStateColor(torrent.state)}">${torrent.state}</span></td>
<td> <td>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}')"> <button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}', ${torrent.category})">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</td> </td>
@@ -162,11 +162,11 @@
} }
} }
async function deleteTorrent(hash) { async function deleteTorrent(hash, category) {
if (!confirm('Are you sure you want to delete this torrent?')) return; if (!confirm('Are you sure you want to delete this torrent?')) return;
try { try {
await fetch(`/internal/torrents/${hash}`, { await fetch(`/internal/torrents/${category}/${hash}`, {
method: 'DELETE' method: 'DELETE'
}); });
await loadTorrents(); await loadTorrents();
@@ -181,10 +181,11 @@
if (!confirm(`Are you sure you want to delete ${state.selectedTorrents.size} selected torrents?`)) return; if (!confirm(`Are you sure you want to delete ${state.selectedTorrents.size} selected torrents?`)) return;
try { try {
const deletePromises = Array.from(state.selectedTorrents).map(hash => // COmma separated list of hashes
fetch(`/internal/torrents/${hash}`, { method: 'DELETE' }) const hashes = Array.from(state.selectedTorrents).join(',');
); await fetch(`/internal/torrents/?hashes=${encodeURIComponent(hashes)}`, {
await Promise.all(deletePromises); method: 'DELETE'
});
await loadTorrents(); await loadTorrents();
createToast('Selected torrents deleted successfully'); createToast('Selected torrents deleted successfully');
} catch (error) { } catch (error) {
+17 -6
View File
@@ -5,6 +5,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/internal/logger" "github.com/sirrobot01/debrid-blackhole/internal/logger"
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
"github.com/sirrobot01/debrid-blackhole/pkg/service" "github.com/sirrobot01/debrid-blackhole/pkg/service"
"os" "os"
"sync" "sync"
@@ -75,6 +76,17 @@ func arrRefreshWorker(ctx context.Context, cfg *config.Config) {
func cleanUpQueuesWorker(ctx context.Context, cfg *config.Config) { func cleanUpQueuesWorker(ctx context.Context, cfg *config.Config) {
// Start Clean up Queues Worker // Start Clean up Queues Worker
_logger := getLogger() _logger := getLogger()
_arrs := service.GetService().Arr
filtered := make([]*arr.Arr, 0)
for _, a := range _arrs.GetAll() {
if a.Cleanup {
filtered = append(filtered, a)
}
}
if len(filtered) == 0 {
_logger.Debug().Msg("No ARR instances configured for cleanup")
return
}
_logger.Debug().Msg("Clean up Queues Worker started") _logger.Debug().Msg("Clean up Queues Worker started")
cleanupCtx := context.WithValue(ctx, "worker", "cleanup") cleanupCtx := context.WithValue(ctx, "worker", "cleanup")
cleanupTicker := time.NewTicker(time.Duration(10) * time.Second) cleanupTicker := time.NewTicker(time.Duration(10) * time.Second)
@@ -90,7 +102,7 @@ func cleanUpQueuesWorker(ctx context.Context, cfg *config.Config) {
if cleanupMutex.TryLock() { if cleanupMutex.TryLock() {
go func() { go func() {
defer cleanupMutex.Unlock() defer cleanupMutex.Unlock()
cleanUpQueues() cleanUpQueues(filtered)
}() }()
} }
} }
@@ -107,13 +119,12 @@ func refreshArrs() {
} }
} }
func cleanUpQueues() { func cleanUpQueues(arrs []*arr.Arr) {
// Clean up queues // Clean up queues
_logger := getLogger() _logger := getLogger()
_logger.Debug().Msg("Cleaning up queues") for _, a := range arrs {
arrs := service.GetService().Arr _logger.Debug().Msgf("Cleaning up queue for %s", a.Name)
for _, arr := range arrs.GetAll() { if err := a.CleanupQueue(); err != nil {
if err := arr.CleanupQueue(); err != nil {
_logger.Debug().Err(err).Msg("Error cleaning up queue") _logger.Debug().Err(err).Msg("Error cleaning up queue")
} }
} }