feat: track download progress on Job (migration + worker poller + API)

Add Job.downloadProgress (0.0-1.0), derived from a worker background
poller that counts audio files landing in staging / expected track
count, since streamrip only reports 0/1. No UI yet (slice B).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-13 23:23:15 +02:00
parent 52e74f7709
commit 9d5e1ff58a
7 changed files with 120 additions and 22 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Job" ADD COLUMN "downloadProgress" DOUBLE PRECISION NOT NULL DEFAULT 0;
+12 -11
View File
@@ -43,17 +43,18 @@ model Request {
}
model Job {
id String @id @default(cuid())
request Request @relation(fields: [requestId], references: [id], onDelete: Cascade)
requestId String @unique
state JobState @default(requested)
currentStage String @default("intake")
attempts Int @default(0)
error String?
claimedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
candidates Candidate[]
id String @id @default(cuid())
request Request @relation(fields: [requestId], references: [id], onDelete: Cascade)
requestId String @unique
state JobState @default(requested)
currentStage String @default("intake")
attempts Int @default(0)
error String?
claimedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
downloadProgress Float @default(0)
candidates Candidate[]
}
model Candidate {
+11 -1
View File
@@ -62,7 +62,15 @@ describe("requests API", () => {
data: {
artist: "Chosen Artist",
album: "Chosen Album",
job: { create: { state: "downloading", currentStage: "download", attempts: 2, error: "boom" } },
job: {
create: {
state: "downloading",
currentStage: "download",
attempts: 2,
error: "boom",
downloadProgress: 0.42,
},
},
},
include: { job: true },
});
@@ -99,6 +107,7 @@ describe("requests API", () => {
expect(found.job.chosen).toEqual({ source: "qobuz", format: "flac", trackCount: 12 });
expect(found.job.error).toBe("boom");
expect(found.job.attempts).toBe(2);
expect(found.job.downloadProgress).toBe(0.42);
});
it("reports no chosen candidate and zero count when a job has none", async () => {
@@ -109,5 +118,6 @@ describe("requests API", () => {
const found = body.requests.find((r: { id: string }) => r.id === created.id);
expect(found.job.candidateCount).toBe(0);
expect(found.job.chosen).toBeNull();
expect(found.job.downloadProgress).toBe(0);
});
});
+1
View File
@@ -63,6 +63,7 @@ export async function GET() {
error: r.job.error,
claimedAt: r.job.claimedAt,
updatedAt: r.job.updatedAt,
downloadProgress: r.job.downloadProgress,
candidateCount: cands.length,
chosen: chosen ? { source: chosen.source, format: chosen.format, trackCount: chosen.trackCount } : null,
};