feat: add functionality to requeue failed jobs via API and UI interface

This commit is contained in:
2026-06-10 23:07:14 +01:00
parent 170f58067e
commit 5aa4982ca6
6 changed files with 94 additions and 1 deletions
+5
View File
@@ -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
)
+12
View File
@@ -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=
+19
View File
@@ -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
+36
View File
@@ -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)
}
+1
View File
@@ -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)
+21 -1
View File
@@ -28,6 +28,7 @@
<th>Saved</th>
<th>Time</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@@ -46,10 +47,15 @@
<td style="color: var(--accent);">{{formatBytes .SizeSaved}}</td>
<td>{{formatDuration .ProcessingTime}}</td>
<td>{{.CreatedAt.Format "Jan 02, 15:04:05"}}</td>
<td>
{{if eq .Status "failed"}}
<button class="btn btn-sm btn-outline" onclick="requeueJob({{.ID}})">Requeue</button>
{{end}}
</td>
</tr>
{{else}}
<tr>
<td colspan="8" style="text-align: center; color: var(--text-secondary);">No job reports found.</td>
<td colspan="9" style="text-align: center; color: var(--text-secondary);">No job reports found.</td>
</tr>
{{end}}
</tbody>
@@ -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);
}
}
</script>
{{end}}