- Revamp decypharr arch \n
- Add callback_ur, download_folder to addContent API \n - Fix few bugs \n - More declarative UI keywords - Speed up repairs - Few other improvements/bug fixes
This commit is contained in:
+70
-49
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirrobot01/decypharr/pkg/store"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -12,34 +13,37 @@ import (
|
||||
"github.com/sirrobot01/decypharr/internal/request"
|
||||
"github.com/sirrobot01/decypharr/internal/utils"
|
||||
"github.com/sirrobot01/decypharr/pkg/arr"
|
||||
"github.com/sirrobot01/decypharr/pkg/qbit"
|
||||
"github.com/sirrobot01/decypharr/pkg/service"
|
||||
"github.com/sirrobot01/decypharr/pkg/version"
|
||||
)
|
||||
|
||||
func (ui *Handler) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
||||
svc := service.GetService()
|
||||
request.JSONResponse(w, svc.Arr.GetAll(), http.StatusOK)
|
||||
func (wb *Web) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
||||
_store := store.GetStore()
|
||||
request.JSONResponse(w, _store.GetArr().GetAll(), http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
svc := service.GetService()
|
||||
_store := store.GetStore()
|
||||
|
||||
results := make([]*qbit.ImportRequest, 0)
|
||||
results := make([]*store.ImportRequest, 0)
|
||||
errs := make([]string, 0)
|
||||
|
||||
arrName := r.FormValue("arr")
|
||||
notSymlink := r.FormValue("notSymlink") == "true"
|
||||
downloadUncached := r.FormValue("downloadUncached") == "true"
|
||||
if arrName == "" {
|
||||
arrName = "uncategorized"
|
||||
debridName := r.FormValue("debrid")
|
||||
callbackUrl := r.FormValue("callbackUrl")
|
||||
downloadFolder := r.FormValue("downloadFolder")
|
||||
if downloadFolder == "" {
|
||||
downloadFolder = config.Get().QBitTorrent.DownloadFolder
|
||||
}
|
||||
|
||||
_arr := svc.Arr.Get(arrName)
|
||||
downloadUncached := r.FormValue("downloadUncached") == "true"
|
||||
|
||||
_arr := _store.GetArr().Get(arrName)
|
||||
if _arr == nil {
|
||||
_arr = arr.New(arrName, "", "", false, false, &downloadUncached)
|
||||
}
|
||||
@@ -59,8 +63,9 @@ func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
errs = append(errs, fmt.Sprintf("Failed to parse URL %s: %v", url, err))
|
||||
continue
|
||||
}
|
||||
importReq := qbit.NewImportRequest(magnet, _arr, !notSymlink, downloadUncached)
|
||||
if err := importReq.Process(ui.qbit); err != nil {
|
||||
|
||||
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, !notSymlink, downloadUncached, callbackUrl, store.ImportTypeAPI)
|
||||
if err := _store.AddTorrent(ctx, importReq); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("URL %s: %v", url, err))
|
||||
continue
|
||||
}
|
||||
@@ -83,8 +88,8 @@ func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
|
||||
importReq := qbit.NewImportRequest(magnet, _arr, !notSymlink, downloadUncached)
|
||||
err = importReq.Process(ui.qbit)
|
||||
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, !notSymlink, downloadUncached, callbackUrl, store.ImportTypeAPI)
|
||||
err = _store.AddTorrent(ctx, importReq)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("File %s: %v", fileHeader.Filename, err))
|
||||
continue
|
||||
@@ -94,27 +99,27 @@ func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
request.JSONResponse(w, struct {
|
||||
Results []*qbit.ImportRequest `json:"results"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Results []*store.ImportRequest `json:"results"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}{
|
||||
Results: results,
|
||||
Errors: errs,
|
||||
}, http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
var req RepairRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
svc := service.GetService()
|
||||
_store := store.GetStore()
|
||||
|
||||
var arrs []string
|
||||
|
||||
if req.ArrName != "" {
|
||||
_arr := svc.Arr.Get(req.ArrName)
|
||||
_arr := _store.GetArr().Get(req.ArrName)
|
||||
if _arr == nil {
|
||||
http.Error(w, "No Arrs found to repair", http.StatusNotFound)
|
||||
return
|
||||
@@ -124,15 +129,15 @@ func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if req.Async {
|
||||
go func() {
|
||||
if err := svc.Repair.AddJob(arrs, req.MediaIds, req.AutoProcess, false); err != nil {
|
||||
ui.logger.Error().Err(err).Msg("Failed to repair media")
|
||||
if err := _store.GetRepair().AddJob(arrs, req.MediaIds, req.AutoProcess, false); err != nil {
|
||||
wb.logger.Error().Err(err).Msg("Failed to repair media")
|
||||
}
|
||||
}()
|
||||
request.JSONResponse(w, "Repair process started", http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if err := svc.Repair.AddJob([]string{req.ArrName}, req.MediaIds, req.AutoProcess, false); err != nil {
|
||||
if err := _store.GetRepair().AddJob([]string{req.ArrName}, req.MediaIds, req.AutoProcess, false); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to repair: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -140,16 +145,16 @@ func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, "Repair completed", http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
v := version.GetInfo()
|
||||
request.JSONResponse(w, v, http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleGetTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, ui.qbit.Storage.GetAllSorted("", "", nil, "added_on", false), http.StatusOK)
|
||||
func (wb *Web) handleGetTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, wb.torrents.GetAllSorted("", "", nil, "added_on", false), http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) {
|
||||
hash := chi.URLParam(r, "hash")
|
||||
category := chi.URLParam(r, "category")
|
||||
removeFromDebrid := r.URL.Query().Get("removeFromDebrid") == "true"
|
||||
@@ -157,11 +162,11 @@ func (ui *Handler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "No hash provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ui.qbit.Storage.Delete(hash, category, removeFromDebrid)
|
||||
wb.torrents.Delete(hash, category, removeFromDebrid)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleDeleteTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleDeleteTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
hashesStr := r.URL.Query().Get("hashes")
|
||||
removeFromDebrid := r.URL.Query().Get("removeFromDebrid") == "true"
|
||||
if hashesStr == "" {
|
||||
@@ -169,15 +174,15 @@ func (ui *Handler) handleDeleteTorrents(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
hashes := strings.Split(hashesStr, ",")
|
||||
ui.qbit.Storage.DeleteMultiple(hashes, removeFromDebrid)
|
||||
wb.torrents.DeleteMultiple(hashes, removeFromDebrid)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
arrCfgs := make([]config.Arr, 0)
|
||||
svc := service.GetService()
|
||||
for _, a := range svc.Arr.GetAll() {
|
||||
_store := store.GetStore()
|
||||
for _, a := range _store.GetArr().GetAll() {
|
||||
arrCfgs = append(arrCfgs, config.Arr{
|
||||
Host: a.Host,
|
||||
Name: a.Name,
|
||||
@@ -191,11 +196,11 @@ func (ui *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, cfg, http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// Decode the JSON body
|
||||
var updatedConfig config.Config
|
||||
if err := json.NewDecoder(r.Body).Decode(&updatedConfig); err != nil {
|
||||
ui.logger.Error().Err(err).Msg("Failed to decode config update request")
|
||||
wb.logger.Error().Err(err).Msg("Failed to decode config update request")
|
||||
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -232,11 +237,12 @@ func (ui *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Update Arrs through the service
|
||||
svc := service.GetService()
|
||||
svc.Arr.Clear() // Clear existing arrs
|
||||
_store := store.GetStore()
|
||||
_arr := _store.GetArr()
|
||||
_arr.Clear() // Clear existing arrs
|
||||
|
||||
for _, a := range updatedConfig.Arrs {
|
||||
svc.Arr.AddOrUpdate(&arr.Arr{
|
||||
_arr.AddOrUpdate(&arr.Arr{
|
||||
Name: a.Name,
|
||||
Host: a.Host,
|
||||
Token: a.Token,
|
||||
@@ -263,25 +269,25 @@ func (ui *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, map[string]string{"status": "success"}, http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleGetRepairJobs(w http.ResponseWriter, r *http.Request) {
|
||||
svc := service.GetService()
|
||||
request.JSONResponse(w, svc.Repair.GetJobs(), http.StatusOK)
|
||||
func (wb *Web) handleGetRepairJobs(w http.ResponseWriter, r *http.Request) {
|
||||
_store := store.GetStore()
|
||||
request.JSONResponse(w, _store.GetRepair().GetJobs(), http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleProcessRepairJob(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleProcessRepairJob(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
svc := service.GetService()
|
||||
if err := svc.Repair.ProcessJob(id); err != nil {
|
||||
ui.logger.Error().Err(err).Msg("Failed to process repair job")
|
||||
_store := store.GetStore()
|
||||
if err := _store.GetRepair().ProcessJob(id); err != nil {
|
||||
wb.logger.Error().Err(err).Msg("Failed to process repair job")
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request) {
|
||||
// Read ids from body
|
||||
var req struct {
|
||||
IDs []string `json:"ids"`
|
||||
@@ -295,7 +301,22 @@ func (ui *Handler) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
svc := service.GetService()
|
||||
svc.Repair.DeleteJobs(req.IDs)
|
||||
_store := store.GetStore()
|
||||
_store.GetRepair().DeleteJobs(req.IDs)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (wb *Web) handleStopRepairJob(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_store := store.GetStore()
|
||||
if err := _store.GetRepair().StopJob(id); err != nil {
|
||||
wb.logger.Error().Err(err).Msg("Failed to stop repair job")
|
||||
http.Error(w, "Failed to stop job: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (ui *Handler) verifyAuth(username, password string) bool {
|
||||
func (wb *Web) verifyAuth(username, password string) bool {
|
||||
// If you're storing hashed password, use bcrypt to compare
|
||||
if username == "" {
|
||||
return false
|
||||
@@ -22,11 +22,11 @@ func (ui *Handler) verifyAuth(username, password string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (ui *Handler) skipAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) skipAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
cfg.UseAuth = false
|
||||
if err := cfg.Save(); err != nil {
|
||||
ui.logger.Error().Err(err).Msg("failed to save config")
|
||||
wb.logger.Error().Err(err).Msg("failed to save config")
|
||||
http.Error(w, "failed to save config", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (ui *Handler) setupMiddleware(next http.Handler) http.Handler {
|
||||
func (wb *Web) setupMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
needsAuth := cfg.NeedsSetup()
|
||||
@@ -24,7 +24,7 @@ func (ui *Handler) setupMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (ui *Handler) authMiddleware(next http.Handler) http.Handler {
|
||||
func (wb *Web) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if setup is needed
|
||||
cfg := config.Get()
|
||||
@@ -38,7 +38,7 @@ func (ui *Handler) authMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
session, _ := wb.cookie.Get(r, "auth-session")
|
||||
auth, ok := session.Values["authenticated"].(bool)
|
||||
|
||||
if !ok || !auth {
|
||||
|
||||
+25
-24
@@ -5,35 +5,36 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (ui *Handler) Routes() http.Handler {
|
||||
func (wb *Web) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Get("/login", ui.LoginHandler)
|
||||
r.Post("/login", ui.LoginHandler)
|
||||
r.Get("/register", ui.RegisterHandler)
|
||||
r.Post("/register", ui.RegisterHandler)
|
||||
r.Get("/skip-auth", ui.skipAuthHandler)
|
||||
r.Get("/version", ui.handleGetVersion)
|
||||
r.Get("/login", wb.LoginHandler)
|
||||
r.Post("/login", wb.LoginHandler)
|
||||
r.Get("/register", wb.RegisterHandler)
|
||||
r.Post("/register", wb.RegisterHandler)
|
||||
r.Get("/skip-auth", wb.skipAuthHandler)
|
||||
r.Get("/version", wb.handleGetVersion)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(ui.authMiddleware)
|
||||
r.Use(ui.setupMiddleware)
|
||||
r.Get("/", ui.IndexHandler)
|
||||
r.Get("/download", ui.DownloadHandler)
|
||||
r.Get("/repair", ui.RepairHandler)
|
||||
r.Get("/config", ui.ConfigHandler)
|
||||
r.Use(wb.authMiddleware)
|
||||
r.Use(wb.setupMiddleware)
|
||||
r.Get("/", wb.IndexHandler)
|
||||
r.Get("/download", wb.DownloadHandler)
|
||||
r.Get("/repair", wb.RepairHandler)
|
||||
r.Get("/config", wb.ConfigHandler)
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Get("/arrs", ui.handleGetArrs)
|
||||
r.Post("/add", ui.handleAddContent)
|
||||
r.Post("/repair", ui.handleRepairMedia)
|
||||
r.Get("/repair/jobs", ui.handleGetRepairJobs)
|
||||
r.Post("/repair/jobs/{id}/process", ui.handleProcessRepairJob)
|
||||
r.Delete("/repair/jobs", ui.handleDeleteRepairJob)
|
||||
r.Get("/torrents", ui.handleGetTorrents)
|
||||
r.Delete("/torrents/{category}/{hash}", ui.handleDeleteTorrent)
|
||||
r.Delete("/torrents/", ui.handleDeleteTorrents)
|
||||
r.Get("/config", ui.handleGetConfig)
|
||||
r.Post("/config", ui.handleUpdateConfig)
|
||||
r.Get("/arrs", wb.handleGetArrs)
|
||||
r.Post("/add", wb.handleAddContent)
|
||||
r.Post("/repair", wb.handleRepairMedia)
|
||||
r.Get("/repair/jobs", wb.handleGetRepairJobs)
|
||||
r.Post("/repair/jobs/{id}/process", wb.handleProcessRepairJob)
|
||||
r.Post("/repair/jobs/{id}/stop", wb.handleStopRepairJob)
|
||||
r.Delete("/repair/jobs", wb.handleDeleteRepairJob)
|
||||
r.Get("/torrents", wb.handleGetTorrents)
|
||||
r.Delete("/torrents/{category}/{hash}", wb.handleDeleteTorrent)
|
||||
r.Delete("/torrents/", wb.handleDeleteTorrents)
|
||||
r.Get("/config", wb.handleGetConfig)
|
||||
r.Post("/config", wb.handleUpdateConfig)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -245,43 +245,48 @@
|
||||
<!-- Step 5: Repair Configuration -->
|
||||
<div class="setup-step d-none" id="step5">
|
||||
<div class="section mb-5">
|
||||
<div class="row mb-2">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<input type="checkbox" class="form-check-input" name="repair.enabled" id="repair.enabled">
|
||||
<label class="form-check-label" for="repair.enabled">Enable Repair</label>
|
||||
<label class="form-check-label" for="repair.enabled">Enable Scheduled Repair</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="repairCol" class="d-none">
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label" for="repair.interval">Interval</label>
|
||||
<label class="form-label" for="repair.interval">Scheduled Interval</label>
|
||||
<input type="text" class="form-control" name="repair.interval" id="repair.interval" placeholder="e.g., 24h">
|
||||
<small class="form-text text-muted">Interval for the repair process(e.g., 24h, 1d, 03:00, or a crontab)</small>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="repair.workers">Workers</label>
|
||||
<input type="text" class="form-control" name="repair.workers" id="repair.workers">
|
||||
<small class="form-text text-muted">Number of workers to use for the repair process</small>
|
||||
</div>
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label" for="repair.zurg_url">Zurg URL</label>
|
||||
<input type="text" class="form-control" name="repair.zurg_url" id="repair.zurg_url" placeholder="http://zurg:9999">
|
||||
<small class="form-text text-muted">Speeds up the repair process by using Zurg</small>
|
||||
<small class="form-text text-muted">If you have Zurg running, you can use it to speed up the repair process</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" name="repair.use_webdav" id="repair.use_webdav">
|
||||
<label class="form-check-label" for="repair.use_webdav">Use Webdav</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Use Internal Webdav for repair(make sure webdav is enabled in the debrid section</small>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" name="repair.run_on_start" id="repair.run_on_start">
|
||||
<label class="form-check-label" for="repair.run_on_start">Run on Start</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Run repair on startup</small>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
|
||||
<label class="form-check-label" for="repair.auto_process">Auto Process</label>
|
||||
@@ -340,7 +345,14 @@
|
||||
<small class="form-text text-muted">Rate limit for the debrid service. Confirm your debrid service rate limit</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check me-3">
|
||||
<input type="checkbox" class="form-check-input useWebdav" name="debrid[${index}].use_webdav" id="debrid[${index}].use_webdav">
|
||||
<label class="form-check-label" for="debrid[${index}].use_webdav">Enable WebDav Server</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Create an internal webdav for this debrid</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check me-3">
|
||||
<input type="checkbox" class="form-check-input" name="debrid[${index}].download_uncached" id="debrid[${index}].download_uncached">
|
||||
@@ -348,13 +360,6 @@
|
||||
</div>
|
||||
<small class="form-text text-muted">Download uncached files from the debrid service</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check me-3">
|
||||
<input type="checkbox" class="form-check-input" name="debrid[${index}].check_cached" id="debrid[${index}].check_cached">
|
||||
<label class="form-check-label" for="debrid[${index}].check_cached" disabled>Check Cached</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Check if the file is cached before downloading(Disabled)</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check me-3">
|
||||
<input type="checkbox" class="form-check-input" name="debrid[${index}].add_samples" id="debrid[${index}].add_samples">
|
||||
@@ -369,16 +374,10 @@
|
||||
</div>
|
||||
<small class="form-text text-muted">Preprocess RARed torrents to allow reading the files inside</small>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check me-3">
|
||||
<input type="checkbox" class="form-check-input useWebdav" name="debrid[${index}].use_webdav" id="debrid[${index}].use_webdav">
|
||||
<label class="form-check-label" for="debrid[${index}].use_webdav">Enable WebDav Server</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Create an internal webdav for this debrid</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webdav d-none">
|
||||
<h6 class="pb-2">Webdav</h6>
|
||||
<div class="webdav d-none mt-1">
|
||||
<hr/>
|
||||
<h6 class="pb-2">Webdav Settings</h6>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].torrents_refresh_interval">Torrents Refresh Interval</label>
|
||||
@@ -441,12 +440,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col mt-3">
|
||||
<h6 class="pb-2">Custom Folders</h6>
|
||||
<div class="col">
|
||||
<h6 class="pb-2">Virtual Folders</h6>
|
||||
<div class="col-12">
|
||||
<p class="text-muted small">Create virtual directories with filters to organize your content</p>
|
||||
<div class="directories-container" id="debrid[${index}].directories">
|
||||
<!-- Dynamic directories will be added here -->
|
||||
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary mt-2 webdav-field" onclick="addDirectory(${index});">
|
||||
<i class="bi bi-plus"></i> Add Directory
|
||||
@@ -842,9 +841,6 @@
|
||||
|
||||
// Load Repair config
|
||||
if (config.repair) {
|
||||
if (config.repair.enabled) {
|
||||
document.getElementById('repairCol').classList.remove('d-none');
|
||||
}
|
||||
Object.entries(config.repair).forEach(([key, value]) => {
|
||||
const input = document.querySelector(`[name="repair.${key}"]`);
|
||||
if (input) {
|
||||
@@ -921,14 +917,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('change', 'input[name="repair.enabled"]', function() {
|
||||
if (this.checked) {
|
||||
$('#repairCol').removeClass('d-none');
|
||||
} else {
|
||||
$('#repairCol').addClass('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
async function saveConfig(e) {
|
||||
const submitButton = e.target.querySelector('button[type="submit"]');
|
||||
submitButton.disabled = true;
|
||||
@@ -1072,7 +1060,7 @@
|
||||
debrids: [],
|
||||
qbittorrent: {
|
||||
download_folder: document.querySelector('[name="qbit.download_folder"]').value,
|
||||
refresh_interval: parseInt(document.querySelector('[name="qbit.refresh_interval"]').value || '0', 10),
|
||||
refresh_interval: parseInt(document.querySelector('[name="qbit.refresh_interval"]').value, 10),
|
||||
max_downloads: parseInt(document.querySelector('[name="qbit.max_downloads"]').value || '0', 5),
|
||||
skip_pre_cache: document.querySelector('[name="qbit.skip_pre_cache"]').checked
|
||||
},
|
||||
@@ -1082,6 +1070,7 @@
|
||||
interval: document.querySelector('[name="repair.interval"]').value,
|
||||
run_on_start: document.querySelector('[name="repair.run_on_start"]').checked,
|
||||
zurg_url: document.querySelector('[name="repair.zurg_url"]').value,
|
||||
workers: parseInt(document.querySelector('[name="repair.workers"]').value),
|
||||
use_webdav: document.querySelector('[name="repair.use_webdav"]').checked,
|
||||
auto_process: document.querySelector('[name="repair.auto_process"]').checked
|
||||
}
|
||||
@@ -1098,7 +1087,6 @@
|
||||
folder: document.querySelector(`[name="debrid[${i}].folder"]`).value,
|
||||
rate_limit: document.querySelector(`[name="debrid[${i}].rate_limit"]`).value,
|
||||
download_uncached: document.querySelector(`[name="debrid[${i}].download_uncached"]`).checked,
|
||||
check_cached: document.querySelector(`[name="debrid[${i}].check_cached"]`).checked,
|
||||
unpack_rar: document.querySelector(`[name="debrid[${i}].unpack_rar"]`).checked,
|
||||
add_samples: document.querySelector(`[name="debrid[${i}].add_samples"]`).checked,
|
||||
use_webdav: document.querySelector(`[name="debrid[${i}].use_webdav"]`).checked
|
||||
|
||||
@@ -17,11 +17,33 @@
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="category" class="form-label">Enter Category</label>
|
||||
<input type="text" class="form-control" id="category" name="arr" placeholder="Enter Category (e.g sonarr, radarr, radarr4k)">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="downloadFolder" class="form-label">Download Folder</label>
|
||||
<input type="text" class="form-control" id="downloadFolder" name="downloadFolder" placeholder="Enter Download Folder (e.g /downloads/torrents)">
|
||||
<small class="text-muted">Default is your qbittorent download_folder</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="arr" class="form-label">Arr (if any)</label>
|
||||
<input type="text" class="form-control" id="arr" name="arr" placeholder="Enter Category (e.g sonarr, radarr, radarr4k)">
|
||||
<small class="text-muted">Optional, leave empty if not using Arr</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if .HasMultiDebrid }}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="debrid" class="form-label">Select Debrid</label>
|
||||
<select class="form-select" id="debrid" name="debrid">
|
||||
{{ range $index, $debrid := .Debrids }}
|
||||
<option value="{{ $debrid }}" {{ if eq $index 0 }}selected{{end}}>{{ $debrid }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<small class="text-muted">Select a debrid service to use for this download</small>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="form-check d-inline-block me-3">
|
||||
@@ -48,23 +70,27 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let downloadFolder = '{{ .DownloadFolder }}';
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loadSavedDownloadOptions = () => {
|
||||
const savedCategory = localStorage.getItem('downloadCategory');
|
||||
const savedSymlink = localStorage.getItem('downloadSymlink');
|
||||
const savedDownloadUncached = localStorage.getItem('downloadUncached');
|
||||
document.getElementById('category').value = savedCategory || '';
|
||||
document.getElementById('arr').value = savedCategory || '';
|
||||
document.getElementById('isSymlink').checked = savedSymlink === 'true';
|
||||
document.getElementById('downloadUncached').checked = savedDownloadUncached === 'true';
|
||||
document.getElementById('downloadFolder').value = localStorage.getItem('downloadFolder') || downloadFolder || '';
|
||||
};
|
||||
|
||||
const saveCurrentDownloadOptions = () => {
|
||||
const category = document.getElementById('category').value;
|
||||
const arr = document.getElementById('arr').value;
|
||||
const isSymlink = document.getElementById('isSymlink').checked;
|
||||
const downloadUncached = document.getElementById('downloadUncached').checked;
|
||||
localStorage.setItem('downloadCategory', category);
|
||||
const downloadFolder = document.getElementById('downloadFolder').value;
|
||||
localStorage.setItem('downloadCategory', arr);
|
||||
localStorage.setItem('downloadSymlink', isSymlink.toString());
|
||||
localStorage.setItem('downloadUncached', downloadUncached.toString());
|
||||
localStorage.setItem('downloadFolder', downloadFolder);
|
||||
};
|
||||
|
||||
// Load the last used download options from local storage
|
||||
@@ -108,9 +134,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append('arr', document.getElementById('category').value);
|
||||
formData.append('arr', document.getElementById('arr').value);
|
||||
formData.append('downloadFolder', document.getElementById('downloadFolder').value);
|
||||
formData.append('notSymlink', document.getElementById('isSymlink').checked);
|
||||
formData.append('downloadUncached', document.getElementById('downloadUncached').checked);
|
||||
formData.append('debrid', document.getElementById('debrid') ? document.getElementById('debrid').value : '');
|
||||
|
||||
const response = await fetcher('/api/add', {
|
||||
method: 'POST',
|
||||
@@ -139,7 +167,7 @@
|
||||
});
|
||||
|
||||
// Save the download options to local storage when they change
|
||||
document.getElementById('category').addEventListener('change', saveCurrentDownloadOptions);
|
||||
document.getElementById('arr').addEventListener('change', saveCurrentDownloadOptions);
|
||||
document.getElementById('isSymlink').addEventListener('change', saveCurrentDownloadOptions);
|
||||
|
||||
// Read the URL parameters for a magnet link and add it to the download queue if found
|
||||
|
||||
@@ -129,11 +129,11 @@
|
||||
<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}', '${torrent.category}', false)">
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}', '${torrent.category || ''}', false)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
${torrent.debrid && torrent.id ? `
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}', '${torrent.category}', true)">
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}', '${torrent.category || ''}', true)">
|
||||
<i class="bi bi-trash"></i> Remove from Debrid
|
||||
</button>
|
||||
` : ''}
|
||||
@@ -485,7 +485,7 @@
|
||||
}
|
||||
},
|
||||
'delete': async (torrent) => {
|
||||
await deleteTorrent(torrent.hash);
|
||||
await deleteTorrent(torrent.hash, torrent.category || '', false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,22 @@
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: var(--bg-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
@@ -193,6 +209,20 @@
|
||||
{{ else }}
|
||||
{{ end }}
|
||||
|
||||
<footer class="mt-auto py-2 text-center border-top">
|
||||
<div class="container">
|
||||
<small class="text-muted">
|
||||
<a href="https://github.com/sirrobot01/decypharr" target="_blank" class="text-decoration-none me-3">
|
||||
<i class="bi bi-github me-1"></i>GitHub
|
||||
</a>
|
||||
<a href="https://sirrobot01.github.io/decypharr" target="_blank" class="text-decoration-none">
|
||||
<i class="bi bi-book me-1"></i>Documentation
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
@@ -143,6 +143,9 @@
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="processJobBtn">Process Items</button>
|
||||
<button type="button" class="btn btn-warning d-none" id="stopJobBtn">
|
||||
<i class="bi bi-stop-fill me-1"></i>Stop Job
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,6 +221,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Return status text and class based on job status
|
||||
function getStatus(status) {
|
||||
switch (status) {
|
||||
case 'started':
|
||||
return {text: 'In Progress', class: 'text-primary'};
|
||||
case 'failed':
|
||||
return {text: 'Failed', class: 'text-danger'};
|
||||
case 'completed':
|
||||
return {text: 'Completed', class: 'text-success'};
|
||||
case 'pending':
|
||||
return {text: 'Pending', class: 'text-warning'};
|
||||
case 'cancelled':
|
||||
return {text: 'Cancelled', class: 'text-secondary'};
|
||||
case 'processing':
|
||||
return {text: 'Processing', class: 'text-info'};
|
||||
default:
|
||||
// Return status in title case if unknown
|
||||
return {text: status.charAt(0).toUpperCase() + status.slice(1), class: 'text-secondary'};
|
||||
}
|
||||
}
|
||||
|
||||
// Render jobs table with pagination
|
||||
function renderJobsTable(page) {
|
||||
const tableBody = document.getElementById('jobsTableBody');
|
||||
@@ -254,24 +278,10 @@
|
||||
const formattedDate = startedDate.toLocaleString();
|
||||
|
||||
// Determine status
|
||||
let status = 'In Progress';
|
||||
let statusClass = 'text-primary';
|
||||
let status = getStatus(job.status);
|
||||
let canDelete = job.status !== "started";
|
||||
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
|
||||
|
||||
if (job.status === 'failed') {
|
||||
status = 'Failed';
|
||||
statusClass = 'text-danger';
|
||||
} else if (job.status === 'completed') {
|
||||
status = 'Completed';
|
||||
statusClass = 'text-success';
|
||||
} else if (job.status === 'pending') {
|
||||
status = 'Pending';
|
||||
statusClass = 'text-warning';
|
||||
} else if (job.status === "processing") {
|
||||
status = 'Processing';
|
||||
statusClass = 'text-info';
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
@@ -283,25 +293,31 @@
|
||||
<td><a href="#" class="text-link view-job" data-id="${job.id}"><small>${job.id.substring(0, 8)}</small></a></td>
|
||||
<td>${job.arrs.join(', ')}</td>
|
||||
<td><small>${formattedDate}</small></td>
|
||||
<td><span class="${statusClass}">${status}</span></td>
|
||||
<td><span class="${status.class}">${status.text}</span></td>
|
||||
<td>${totalItems}</td>
|
||||
<td>
|
||||
${job.status === "pending" ?
|
||||
`<button class="btn btn-sm btn-primary process-job" data-id="${job.id}">
|
||||
<i class="bi bi-play-fill"></i> Process
|
||||
`<button class="btn btn-sm btn-primary process-job" data-id="${job.id}">
|
||||
<i class="bi bi-play-fill"></i> Process
|
||||
</button>` :
|
||||
`<button class="btn btn-sm btn-primary" disabled>
|
||||
<i class="bi bi-eye"></i> Process
|
||||
`<button class="btn btn-sm btn-primary" disabled>
|
||||
<i class="bi bi-eye"></i> Process
|
||||
</button>`
|
||||
}
|
||||
}
|
||||
${(job.status === "started" || job.status === "processing") ?
|
||||
`<button class="btn btn-sm btn-warning stop-job" data-id="${job.id}">
|
||||
<i class="bi bi-stop-fill"></i> Stop
|
||||
</button>` :
|
||||
''
|
||||
}
|
||||
${canDelete ?
|
||||
`<button class="btn btn-sm btn-danger delete-job" data-id="${job.id}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>` :
|
||||
`<button class="btn btn-sm btn-danger" disabled>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>`
|
||||
}
|
||||
`<button class="btn btn-sm btn-danger delete-job" data-id="${job.id}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>` :
|
||||
`<button class="btn btn-sm btn-danger" disabled>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>`
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
|
||||
@@ -370,6 +386,13 @@
|
||||
viewJobDetails(jobId);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.stop-job').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const jobId = e.currentTarget.dataset.id;
|
||||
stopJob(jobId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('selectAllJobs').addEventListener('change', function() {
|
||||
@@ -456,6 +479,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function stopJob(jobId) {
|
||||
if (confirm('Are you sure you want to stop this job?')) {
|
||||
try {
|
||||
const response = await fetcher(`/api/repair/jobs/${jobId}/stop`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
createToast('Job stop requested successfully');
|
||||
await loadJobs(currentPage); // Refresh the jobs list
|
||||
} catch (error) {
|
||||
createToast(`Error stopping job: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View job details function
|
||||
function viewJobDetails(jobId) {
|
||||
// Find the job
|
||||
@@ -477,24 +519,9 @@
|
||||
}
|
||||
|
||||
// Set status with color
|
||||
let status = 'In Progress';
|
||||
let statusClass = 'text-primary';
|
||||
let status = getStatus(job.status);
|
||||
|
||||
if (job.status === 'failed') {
|
||||
status = 'Failed';
|
||||
statusClass = 'text-danger';
|
||||
} else if (job.status === 'completed') {
|
||||
status = 'Completed';
|
||||
statusClass = 'text-success';
|
||||
} else if (job.status === 'pending') {
|
||||
status = 'Pending';
|
||||
statusClass = 'text-warning';
|
||||
} else if (job.status === "processing") {
|
||||
status = 'Processing';
|
||||
statusClass = 'text-info';
|
||||
}
|
||||
|
||||
document.getElementById('modalJobStatus').innerHTML = `<span class="${statusClass}">${status}</span>`;
|
||||
document.getElementById('modalJobStatus').innerHTML = `<span class="${status.class}">${status.text}</span>`;
|
||||
|
||||
// Set other job details
|
||||
document.getElementById('modalJobArrs').textContent = job.arrs.join(', ');
|
||||
@@ -524,6 +551,19 @@
|
||||
processBtn.classList.add('d-none');
|
||||
}
|
||||
|
||||
// Stop button visibility
|
||||
const stopBtn = document.getElementById('stopJobBtn'); // You'll need to add this button to the HTML
|
||||
if (job.status === 'started' || job.status === 'processing') {
|
||||
stopBtn.classList.remove('d-none');
|
||||
stopBtn.onclick = () => {
|
||||
stopJob(job.id);
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('jobDetailsModal'));
|
||||
modal.hide();
|
||||
};
|
||||
} else {
|
||||
stopBtn.classList.add('d-none');
|
||||
}
|
||||
|
||||
// Populate broken items table
|
||||
const brokenItemsTableBody = document.getElementById('brokenItemsTableBody');
|
||||
const noBrokenItemsMessage = document.getElementById('noBrokenItemsMessage');
|
||||
|
||||
+28
-21
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
if cfg.NeedsAuth() {
|
||||
http.Redirect(w, r, "/register", http.StatusSeeOther)
|
||||
@@ -19,7 +19,7 @@ func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
"Page": "login",
|
||||
"Title": "Login",
|
||||
}
|
||||
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||
_ = wb.templates.ExecuteTemplate(w, "layout", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if ui.verifyAuth(credentials.Username, credentials.Password) {
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
if wb.verifyAuth(credentials.Username, credentials.Password) {
|
||||
session, _ := wb.cookie.Get(r, "auth-session")
|
||||
session.Values["authenticated"] = true
|
||||
session.Values["username"] = credentials.Username
|
||||
if err := session.Save(r, w); err != nil {
|
||||
@@ -48,8 +48,8 @@ func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (ui *Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
func (wb *Web) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := wb.cookie.Get(r, "auth-session")
|
||||
session.Values["authenticated"] = false
|
||||
session.Options.MaxAge = -1
|
||||
err := session.Save(r, w)
|
||||
@@ -59,7 +59,7 @@ func (ui *Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ui *Handler) RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
authCfg := cfg.GetAuth()
|
||||
|
||||
@@ -69,7 +69,7 @@ func (ui *Handler) RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
"Page": "register",
|
||||
"Title": "Register",
|
||||
}
|
||||
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||
_ = wb.templates.ExecuteTemplate(w, "layout", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func (ui *Handler) RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Create a session
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
session, _ := wb.cookie.Get(r, "auth-session")
|
||||
session.Values["authenticated"] = true
|
||||
session.Values["username"] = username
|
||||
if err := session.Save(r, w); err != nil {
|
||||
@@ -110,42 +110,49 @@ func (ui *Handler) RegisterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (ui *Handler) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
data := map[string]interface{}{
|
||||
"URLBase": cfg.URLBase,
|
||||
"Page": "index",
|
||||
"Title": "Torrents",
|
||||
}
|
||||
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||
_ = wb.templates.ExecuteTemplate(w, "layout", data)
|
||||
}
|
||||
|
||||
func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
data := map[string]interface{}{
|
||||
"URLBase": cfg.URLBase,
|
||||
"Page": "download",
|
||||
"Title": "Download",
|
||||
debrids := make([]string, 0)
|
||||
for _, d := range cfg.Debrids {
|
||||
debrids = append(debrids, d.Name)
|
||||
}
|
||||
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||
data := map[string]interface{}{
|
||||
"URLBase": cfg.URLBase,
|
||||
"Page": "download",
|
||||
"Title": "Download",
|
||||
"Debrids": debrids,
|
||||
"HasMultiDebrid": len(debrids) > 1,
|
||||
"DownloadFolder": cfg.QBitTorrent.DownloadFolder,
|
||||
}
|
||||
_ = wb.templates.ExecuteTemplate(w, "layout", data)
|
||||
}
|
||||
|
||||
func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
data := map[string]interface{}{
|
||||
"URLBase": cfg.URLBase,
|
||||
"Page": "repair",
|
||||
"Title": "Repair",
|
||||
}
|
||||
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||
_ = wb.templates.ExecuteTemplate(w, "layout", data)
|
||||
}
|
||||
|
||||
func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (wb *Web) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
data := map[string]interface{}{
|
||||
"URLBase": cfg.URLBase,
|
||||
"Page": "config",
|
||||
"Title": "Config",
|
||||
}
|
||||
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||
_ = wb.templates.ExecuteTemplate(w, "layout", data)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/decypharr/internal/logger"
|
||||
"github.com/sirrobot01/decypharr/pkg/qbit"
|
||||
"github.com/sirrobot01/decypharr/pkg/store"
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
@@ -50,26 +50,15 @@ type RepairRequest struct {
|
||||
//go:embed templates/*
|
||||
var content embed.FS
|
||||
|
||||
type Handler struct {
|
||||
qbit *qbit.QBit
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func New(qbit *qbit.QBit) *Handler {
|
||||
return &Handler{
|
||||
qbit: qbit,
|
||||
logger: logger.New("ui"),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
secretKey = cmp.Or(os.Getenv("DECYPHARR_SECRET_KEY"), "\"wqj(v%lj*!-+kf@4&i95rhh_!5_px5qnuwqbr%cjrvrozz_r*(\"")
|
||||
store = sessions.NewCookieStore([]byte(secretKey))
|
||||
type Web struct {
|
||||
logger zerolog.Logger
|
||||
cookie *sessions.CookieStore
|
||||
templates *template.Template
|
||||
)
|
||||
torrents *store.TorrentStorage
|
||||
}
|
||||
|
||||
func init() {
|
||||
templates = template.Must(template.ParseFS(
|
||||
func New() *Web {
|
||||
templates := template.Must(template.ParseFS(
|
||||
content,
|
||||
"templates/layout.html",
|
||||
"templates/index.html",
|
||||
@@ -79,10 +68,17 @@ func init() {
|
||||
"templates/login.html",
|
||||
"templates/register.html",
|
||||
))
|
||||
|
||||
store.Options = &sessions.Options{
|
||||
secretKey := cmp.Or(os.Getenv("DECYPHARR_SECRET_KEY"), "\"wqj(v%lj*!-+kf@4&i95rhh_!5_px5qnuwqbr%cjrvrozz_r*(\"")
|
||||
cookieStore := sessions.NewCookieStore([]byte(secretKey))
|
||||
cookieStore.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 7,
|
||||
HttpOnly: false,
|
||||
}
|
||||
return &Web{
|
||||
logger: logger.New("ui"),
|
||||
templates: templates,
|
||||
cookie: cookieStore,
|
||||
torrents: store.GetStore().GetTorrentStorage(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user