diff --git a/public/styles.css b/public/styles.css
index 61b1c62..4422778 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -72,9 +72,11 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe
.task-item.complete .task-bar { background: #4e8; }
.slow-queue-section { margin-top: 0.5rem; border-top: 1px solid #333; padding-top: 0.5rem; }
.slow-queue-section.hidden { display: none; }
-.slow-queue-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.3rem; padding: 0 0.2rem; }
+.slow-queue-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.3rem; padding: 0 0.2rem; gap: 0.5rem; }
.slow-queue-title { font-size: 0.75rem; color: #888; font-weight: 500; }
-.slow-queue-timer { font-size: 0.7rem; color: #6af; }
+.slow-queue-timer { font-size: 0.7rem; color: #6af; flex: 1; }
+.slow-queue-cancel-all { background: none; border: 1px solid #633; color: #a66; font-size: 0.65rem; padding: 0.15rem 0.4rem; border-radius: 3px; cursor: pointer; transition: all 0.15s; }
+.slow-queue-cancel-all:hover { background: #422; border-color: #844; color: #e88; }
.slow-queue-list { display: flex; flex-direction: column; gap: 0.15rem; max-height: 200px; overflow-y: auto; }
.slow-queue-playlist-header { font-size: 0.7rem; color: #888; padding: 0.3rem 0.2rem 0.15rem; margin-top: 0.2rem; border-top: 1px solid #2a2a2a; }
.slow-queue-playlist-header:first-child { border-top: none; margin-top: 0; }
diff --git a/public/upload.js b/public/upload.js
index 45d4854..0743d7a 100644
--- a/public/upload.js
+++ b/public/upload.js
@@ -222,10 +222,27 @@
`;
+ // Wire up cancel all button
+ slowQueueSection.querySelector(".slow-queue-cancel-all").onclick = async () => {
+ try {
+ const res = await fetch("/api/fetch", { method: "DELETE" });
+ if (res.ok) {
+ const data = await res.json();
+ M.showToast(data.message);
+ pollSlowQueue();
+ } else {
+ M.showToast("Failed to cancel", "error");
+ }
+ } catch (e) {
+ M.showToast("Failed to cancel", "error");
+ }
+ };
+
// Insert before tasks-empty
tasksEmpty.parentNode.insertBefore(slowQueueSection, tasksEmpty);
return slowQueueSection;
diff --git a/routes/fetch.ts b/routes/fetch.ts
index afd323e..f79b0bb 100644
--- a/routes/fetch.ts
+++ b/routes/fetch.ts
@@ -6,6 +6,7 @@ import {
addToSlowQueue,
getQueues,
cancelSlowQueueItem,
+ cancelAllSlowQueueItems,
} from "../ytdlp";
import { getOrCreateUser } from "./helpers";
import { createPlaylist, generateUniquePlaylistName } from "../db";
@@ -133,3 +134,14 @@ export function handleCancelFetchItem(req: Request, server: any, itemId: string)
return Response.json({ error: "Cannot cancel item (not found, not owned, or already downloading)" }, { status: 400, headers });
}
}
+
+// DELETE /api/fetch - cancel all slow queue items for user
+export function handleCancelAllFetchItems(req: Request, server: any): Response {
+ const { user, headers } = getOrCreateUser(req, server);
+ if (!user) {
+ return Response.json({ error: "Authentication required" }, { status: 401 });
+ }
+
+ const cancelled = cancelAllSlowQueueItems(user.id);
+ return Response.json({ message: `Cancelled ${cancelled} items`, cancelled }, { headers });
+}
diff --git a/routes/index.ts b/routes/index.ts
index 08a0cf8..351e57d 100644
--- a/routes/index.ts
+++ b/routes/index.ts
@@ -41,6 +41,7 @@ import {
handleFetchConfirm,
handleGetFetchQueue,
handleCancelFetchItem,
+ handleCancelAllFetchItems,
} from "./fetch";
// Playlist routes
@@ -150,6 +151,9 @@ export function createRouter() {
if (path === "/api/fetch" && req.method === "GET") {
return handleGetFetchQueue(req, server);
}
+ if (path === "/api/fetch" && req.method === "DELETE") {
+ return handleCancelAllFetchItems(req, server);
+ }
const fetchCancelMatch = path.match(/^\/api\/fetch\/([^/]+)$/);
if (fetchCancelMatch && req.method === "DELETE") {
return handleCancelFetchItem(req, server, fetchCancelMatch[1]);
diff --git a/ytdlp.ts b/ytdlp.ts
index c428556..836729c 100644
--- a/ytdlp.ts
+++ b/ytdlp.ts
@@ -518,6 +518,36 @@ export function cancelSlowQueueItem(id: string, userId: number): boolean {
return true;
}
+// Cancel all queued items in slow queue for a user
+export function cancelAllSlowQueueItems(userId: number): number {
+ const items = slowQueue.filter(i => i.userId === userId && i.status === "queued");
+ let cancelled = 0;
+
+ for (const item of items) {
+ item.status = "cancelled";
+ item.completedAt = Date.now();
+
+ updateSlowQueueItem(item.id, {
+ status: "cancelled",
+ completedAt: Math.floor(item.completedAt / 1000)
+ });
+
+ notifyProgress(item);
+ cancelled++;
+ }
+
+ // Remove all cancelled items after brief delay
+ setTimeout(() => {
+ for (let i = slowQueue.length - 1; i >= 0; i--) {
+ if (slowQueue[i].status === "cancelled") {
+ slowQueue.splice(i, 1);
+ }
+ }
+ }, 1000);
+
+ return cancelled;
+}
+
// Notify progress callback
function notifyProgress(item: QueueItem): void {
if (onProgress) {