diff --git a/go.mod b/go.mod index bb02ed3..587b7b1 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,8 @@ require ( github.com/go-sql-driver/mysql v1.8.1 gopkg.in/yaml.v3 v3.0.1 ) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + golang.org/x/sys v0.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..37c1d47 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/db/queries.go b/internal/db/queries.go index f64a98f..3b9d50a 100644 --- a/internal/db/queries.go +++ b/internal/db/queries.go @@ -195,6 +195,25 @@ func GetJobReports(limit, offset int, statusFilter string) ([]JobReport, int, er return reports, total, nil } +func GetJobReportByID(id int) (JobReport, error) { + var r JobReport + var targetRes, ffmpegFlags, errMsg sql.NullString + err := DB.QueryRow(`SELECT id, file_path, media_type, status, original_size, encoded_size, size_saved, processing_time, target_resolution, ffmpeg_flags, error_message, created_at FROM job_reports WHERE id = ?`, id). + Scan(&r.ID, &r.FilePath, &r.MediaType, &r.Status, &r.OriginalSize, &r.EncodedSize, &r.SizeSaved, &r.ProcessingTime, &targetRes, &ffmpegFlags, &errMsg, &r.CreatedAt) + if err != nil { + return r, err + } + if targetRes.Valid { r.TargetResolution = targetRes.String } + if ffmpegFlags.Valid { r.FFmpegFlags = ffmpegFlags.String } + if errMsg.Valid { r.ErrorMessage = errMsg.String } + return r, nil +} + +func DeleteJobReport(id int) error { + _, err := DB.Exec(`DELETE FROM job_reports WHERE id = ?`, id) + return err +} + type DashboardStats struct { TotalSavedSpace int64 FilesEncoded int diff --git a/internal/web/api.go b/internal/web/api.go index 0b44e45..417f17a 100644 --- a/internal/web/api.go +++ b/internal/web/api.go @@ -130,3 +130,39 @@ func (s *Server) handleDeleteWatchFolder(w http.ResponseWriter, r *http.Request) s.wm.Reload() w.WriteHeader(http.StatusOK) } + +func (s *Server) handleRequeueJob(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + id, err := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/jobs/requeue/")) + if err != nil { + http.Error(w, "Invalid ID", http.StatusBadRequest) + return + } + + report, err := db.GetJobReportByID(id) + if err != nil { + http.Error(w, "Job report not found", http.StatusNotFound) + return + } + + if report.Status != "failed" { + http.Error(w, "Can only requeue failed jobs", http.StatusBadRequest) + return + } + + err = db.AddJob(report.FilePath, report.MediaType, 5, report.TargetResolution, report.FFmpegFlags, report.OriginalSize) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Delete the failed report so it doesn't show up in history as failed anymore + _ = db.DeleteJobReport(id) + + s.qm.NotifySSE("queue_updated", nil) + s.qm.NotifySSE("job_added", nil) + w.WriteHeader(http.StatusOK) +} diff --git a/internal/web/server.go b/internal/web/server.go index 9a83375..7946ad4 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -132,6 +132,7 @@ func (s *Server) routes() { s.mux.HandleFunc("/api/status", s.handleStatus) s.mux.HandleFunc("/api/jobs/bump/", s.handleBumpJob) s.mux.HandleFunc("/api/jobs/cancel/", s.handleCancelJob) + s.mux.HandleFunc("/api/jobs/requeue/", s.handleRequeueJob) s.mux.HandleFunc("/api/folders", s.handleGetWatchFolders) s.mux.HandleFunc("/api/folders/add", s.handleAddWatchFolder) diff --git a/web/templates/history.html b/web/templates/history.html index 53b8a32..8d330a2 100644 --- a/web/templates/history.html +++ b/web/templates/history.html @@ -28,6 +28,7 @@ Saved Time Date + Actions @@ -46,10 +47,15 @@ {{formatBytes .SizeSaved}} {{formatDuration .ProcessingTime}} {{.CreatedAt.Format "Jan 02, 15:04:05"}} + + {{if eq .Status "failed"}} + + {{end}} + {{else}} - No job reports found. + No job reports found. {{end}} @@ -110,5 +116,19 @@ function filterHistory() { // Update the record count document.getElementById("record-count").textContent = filter ? visibleCount : "{{.Total}}"; } + +async function requeueJob(id) { + if (!confirm("Requeue this failed job?")) return; + try { + const res = await fetch('/api/jobs/requeue/' + id, { method: 'POST' }); + if (res.ok) { + window.location.reload(); + } else { + alert("Failed to requeue job: " + await res.text()); + } + } catch (e) { + alert("Error: " + e.message); + } +} {{end}}