c49cee9a20
Show a live "~Xm left" countdown on the download progress bar. The slskd download loop already polls byte transfer every 3s; measure the actual throughput (EWMA- smoothed bytes/sec), compute seconds-remaining from the album's total bytes, and surface it. New nullable Job.downloadEtaSeconds (migration; web entrypoint runs prisma migrate deploy), threaded through the on_progress callback (optional 2nd arg, so other adapters/callers are unaffected — they report no ETA). API exposes it; queue.tsx renders etaLabel() after the track count. Null when unknown (no sample yet or a source that doesn't report bytes). Worker 330 tests, web 214 tests, both green; verified live-rendered as "42% · 6/14 tracks · ~4m left". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
|
|
export async function POST(request: Request) {
|
|
let body: unknown;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return Response.json({ error: "invalid JSON" }, { status: 400 });
|
|
}
|
|
|
|
const { artist, album } = (body ?? {}) as { artist?: unknown; album?: unknown };
|
|
if (typeof artist !== "string" || !artist.trim() || typeof album !== "string" || !album.trim()) {
|
|
return Response.json({ error: "artist and album are required" }, { status: 400 });
|
|
}
|
|
|
|
const created = await prisma.request.create({
|
|
data: {
|
|
artist: artist.trim(),
|
|
album: album.trim(),
|
|
job: { create: {} },
|
|
},
|
|
include: { job: true },
|
|
});
|
|
|
|
return Response.json(
|
|
{
|
|
id: created.id,
|
|
artist: created.artist,
|
|
album: created.album,
|
|
status: created.status,
|
|
job: { id: created.job!.id, state: created.job!.state, currentStage: created.job!.currentStage },
|
|
},
|
|
{ status: 201 },
|
|
);
|
|
}
|
|
|
|
export async function GET() {
|
|
const [requests, releases] = await Promise.all([
|
|
prisma.request.findMany({
|
|
orderBy: { createdAt: "desc" },
|
|
include: { job: { include: { candidates: { select: { source: true, format: true, trackCount: true, chosen: true } } } } },
|
|
}),
|
|
prisma.monitoredRelease.findMany({ select: { artistName: true, album: true, rgMbid: true } }),
|
|
]);
|
|
const rgByKey = new Map(releases.map((mr) => [`${mr.artistName} ${mr.album}`, mr.rgMbid]));
|
|
|
|
return Response.json({
|
|
requests: requests.map((r) => ({
|
|
id: r.id,
|
|
artist: r.artist,
|
|
album: r.album,
|
|
status: r.status,
|
|
createdAt: r.createdAt,
|
|
rgMbid: rgByKey.get(`${r.artist} ${r.album}`) ?? null,
|
|
job: r.job
|
|
? (() => {
|
|
const cands = r.job.candidates;
|
|
const chosen = cands.find((c) => c.chosen) ?? null;
|
|
return {
|
|
state: r.job.state,
|
|
currentStage: r.job.currentStage,
|
|
attempts: r.job.attempts,
|
|
error: r.job.error,
|
|
claimedAt: r.job.claimedAt,
|
|
updatedAt: r.job.updatedAt,
|
|
downloadProgress: r.job.downloadProgress,
|
|
downloadEtaSeconds: r.job.downloadEtaSeconds,
|
|
paused: r.job.paused,
|
|
candidateCount: cands.length,
|
|
chosen: chosen ? { source: chosen.source, format: chosen.format, trackCount: chosen.trackCount } : null,
|
|
};
|
|
})()
|
|
: null,
|
|
})),
|
|
});
|
|
}
|