diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12237bb..9265b7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+## 0.48.0 - 2026-07-09
+
+- Added a `Clear failed` bulk action to the Queue page so failed jobs can be removed without touching pending, running, finished, or canceled downloads.
+
## 0.47.1 - 2026-07-09
- Fixed `anipy-cli` fallback renaming for queued TV downloads so retried jobs now keep the requested episode numbers from the queue, avoiding cases like a retried `S02E11` being renamed as `S02E01`.
diff --git a/README.md b/README.md
index b52ab37..722189c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Local web UI for a system-wide `ani-cli` install.
-Current version: `0.47.1`
+Current version: `0.48.0`
## What it does
@@ -19,7 +19,7 @@ Current version: `0.47.1`
## Pages
- `/`: Search and queue downloads, with poster-card search results below the selection box and alternate English titles under the main name when available
-- `/queue`: Monitor jobs, inspect logs, retry failed jobs, remove finished jobs
+- `/queue`: Monitor jobs, inspect logs, retry failed jobs, clear failed jobs, and remove finished jobs
- `/watchlist`: Track shows, refresh them, download all available episodes, and manage auto-download settings such as `Source name`, library `Name`, season, and episode offset
- `/config`: Save defaults, toggle `anipy-cli` fallback retries, schedule refreshes, configure Jellyfin and Discord, monitor Jellyfin handoff progress, and view runtime info and the changelog
diff --git a/VERSION b/VERSION
index 650298f..a758a09 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.47.1
+0.48.0
diff --git a/http_handler.py b/http_handler.py
index 8af15e2..c8d7c85 100644
--- a/http_handler.py
+++ b/http_handler.py
@@ -704,6 +704,9 @@ class Handler(BaseHTTPRequestHandler):
elif parsed.path == "/api/queue/clear-finished":
runtime = Handler._runtime(self)
self.json(runtime["download_queue"].clear_finished())
+ elif parsed.path == "/api/queue/clear-failed":
+ runtime = Handler._runtime(self)
+ self.json(runtime["download_queue"].clear_failed())
elif parsed.path.startswith("/api/queue/"):
runtime = Handler._runtime(self)
parts = parsed.path.strip("/").split("/")
diff --git a/queue_jobs.py b/queue_jobs.py
index 7665bf6..1c5beaa 100644
--- a/queue_jobs.py
+++ b/queue_jobs.py
@@ -1291,6 +1291,11 @@ class DownloadQueue:
cursor = conn.execute("DELETE FROM jobs WHERE status IN ('done', 'canceled')")
return {"ok": True, "count": cursor.rowcount}
+ def clear_failed(self):
+ with self.lock, self._connect() as conn:
+ cursor = conn.execute("DELETE FROM jobs WHERE status = 'failed'")
+ return {"ok": True, "count": cursor.rowcount}
+
def _find(self, job_id):
with self._connect() as conn:
row = conn.execute("SELECT * FROM jobs WHERE id = ?", (job_id,)).fetchone()
diff --git a/queue_page.py b/queue_page.py
index 36c0129..ae4ee9c 100644
--- a/queue_page.py
+++ b/queue_page.py
@@ -317,6 +317,7 @@ QUEUE_HTML = r"""
Monitor active downloads, inspect recent output, and clean up finished jobs without leaving the queue view.