Add direct file download links to completed requests

Embeds a signed JWT download token (30-day expiry) in the requests API
response so users can download completed audiobook/ebook files directly
from the UI or by sharing the URL to apps like BookPlayer — no session
cookie required.

- jwt.ts: add generateDownloadToken / verifyDownloadToken helpers
- api/requests: append downloadUrl to completed requests with a filePath
- api/requests/[id]/download: new token-authenticated streaming endpoint;
  serves single files directly or zips multi-file audiobooks with adm-zip
- RequestCard: add Download link in the actions area for completed requests
This commit is contained in:
razzamatazm
2026-02-26 11:21:06 -08:00
parent 547af71de8
commit 1006a04337
4 changed files with 201 additions and 2 deletions
+16
View File
@@ -26,12 +26,15 @@ interface RequestCardProps {
createdAt: string;
updatedAt: string;
completedAt?: string;
downloadUrl?: string | null;
audiobook: {
id: string;
audibleAsin?: string;
title: string;
author: string;
coverArtUrl?: string;
filePath?: string | null;
fileFormat?: string | null;
};
};
showActions?: boolean;
@@ -49,6 +52,7 @@ export function RequestCard({ request, showActions = true }: RequestCardProps) {
const requestType = request.type || 'audiobook';
const isEbook = requestType === 'ebook';
const isCompleted = ['available', 'downloaded'].includes(request.status);
const canCancel = ['pending', 'searching', 'downloading'].includes(request.status);
const isActive = ['searching', 'downloading', 'processing'].includes(request.status);
const isFailed = request.status === 'failed';
@@ -271,6 +275,18 @@ export function RequestCard({ request, showActions = true }: RequestCardProps) {
</Button>
</>
)}
{isCompleted && request.downloadUrl && (
<a
href={request.downloadUrl}
className="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download
</a>
)}
{canCancel && (
<Button
onClick={handleCancel}