dev/playlists #13
|
|
@ -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; }
|
.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 { margin-top: 0.5rem; border-top: 1px solid #333; padding-top: 0.5rem; }
|
||||||
.slow-queue-section.hidden { display: none; }
|
.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-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-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 { 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; }
|
.slow-queue-playlist-header:first-child { border-top: none; margin-top: 0; }
|
||||||
|
|
|
||||||
|
|
@ -222,10 +222,27 @@
|
||||||
<div class="slow-queue-header">
|
<div class="slow-queue-header">
|
||||||
<span class="slow-queue-title">Playlist Queue</span>
|
<span class="slow-queue-title">Playlist Queue</span>
|
||||||
<span class="slow-queue-timer"></span>
|
<span class="slow-queue-timer"></span>
|
||||||
|
<button class="slow-queue-cancel-all" title="Cancel all">Cancel All</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="slow-queue-list"></div>
|
<div class="slow-queue-list"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 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
|
// Insert before tasks-empty
|
||||||
tasksEmpty.parentNode.insertBefore(slowQueueSection, tasksEmpty);
|
tasksEmpty.parentNode.insertBefore(slowQueueSection, tasksEmpty);
|
||||||
return slowQueueSection;
|
return slowQueueSection;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
addToSlowQueue,
|
addToSlowQueue,
|
||||||
getQueues,
|
getQueues,
|
||||||
cancelSlowQueueItem,
|
cancelSlowQueueItem,
|
||||||
|
cancelAllSlowQueueItems,
|
||||||
} from "../ytdlp";
|
} from "../ytdlp";
|
||||||
import { getOrCreateUser } from "./helpers";
|
import { getOrCreateUser } from "./helpers";
|
||||||
import { createPlaylist, generateUniquePlaylistName } from "../db";
|
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 });
|
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 });
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import {
|
||||||
handleFetchConfirm,
|
handleFetchConfirm,
|
||||||
handleGetFetchQueue,
|
handleGetFetchQueue,
|
||||||
handleCancelFetchItem,
|
handleCancelFetchItem,
|
||||||
|
handleCancelAllFetchItems,
|
||||||
} from "./fetch";
|
} from "./fetch";
|
||||||
|
|
||||||
// Playlist routes
|
// Playlist routes
|
||||||
|
|
@ -150,6 +151,9 @@ export function createRouter() {
|
||||||
if (path === "/api/fetch" && req.method === "GET") {
|
if (path === "/api/fetch" && req.method === "GET") {
|
||||||
return handleGetFetchQueue(req, server);
|
return handleGetFetchQueue(req, server);
|
||||||
}
|
}
|
||||||
|
if (path === "/api/fetch" && req.method === "DELETE") {
|
||||||
|
return handleCancelAllFetchItems(req, server);
|
||||||
|
}
|
||||||
const fetchCancelMatch = path.match(/^\/api\/fetch\/([^/]+)$/);
|
const fetchCancelMatch = path.match(/^\/api\/fetch\/([^/]+)$/);
|
||||||
if (fetchCancelMatch && req.method === "DELETE") {
|
if (fetchCancelMatch && req.method === "DELETE") {
|
||||||
return handleCancelFetchItem(req, server, fetchCancelMatch[1]);
|
return handleCancelFetchItem(req, server, fetchCancelMatch[1]);
|
||||||
|
|
|
||||||
30
ytdlp.ts
30
ytdlp.ts
|
|
@ -518,6 +518,36 @@ export function cancelSlowQueueItem(id: string, userId: number): boolean {
|
||||||
return true;
|
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
|
// Notify progress callback
|
||||||
function notifyProgress(item: QueueItem): void {
|
function notifyProgress(item: QueueItem): void {
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue