fix: bulk video download (original quality, latest version by default)

- Accept source=auto in the version download route (was 400 Bad Request),
  so bulk/project downloads of Bunny videos no longer fail.
- Bulk/project downloads now request the original (uncompressed) Bunny file
  so quality never drops (was source=auto which could fall back to compressed).
- Project/selected downloads default to the latest version of each video and
  add a separate "All versions" option in the download dropdowns.
This commit is contained in:
yusufipk
2026-07-10 20:04:18 +07:00
parent cbaecb92cc
commit 34e72f6cbb
4 changed files with 98 additions and 37 deletions
@@ -18,11 +18,18 @@ import {
Download,
Loader2,
Trash2,
ChevronDown,
} from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
AlertDialog,
AlertDialogAction,
@@ -167,13 +174,17 @@ export function ProjectContentClient({
}, []);
const startProjectDownload = useCallback(
async (videoIds?: string[]) => {
async (videoIds?: string[], options?: { allVersions?: boolean }) => {
if (!canDownloadProject || isDownloading) return;
const query =
videoIds && videoIds.length > 0
? `?videoIds=${encodeURIComponent(videoIds.join(','))}`
: '';
const searchParams = new URLSearchParams();
if (videoIds && videoIds.length > 0) {
searchParams.set('videoIds', videoIds.join(','));
}
if (options?.allVersions) {
searchParams.set('versions', 'all');
}
const query = searchParams.toString() ? `?${searchParams.toString()}` : '';
setIsDownloading(true);
try {
@@ -325,19 +336,27 @@ export function ProjectContentClient({
)}
</Button>
{canDownloadProject && localVideos.length > 0 && !selectionMode && (
<Button
variant="outline"
size="sm"
onClick={() => startProjectDownload()}
disabled={isDownloading}
>
{isDownloading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Download project
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={isDownloading}>
{isDownloading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Download project
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => startProjectDownload()}>
Latest version only
</DropdownMenuItem>
<DropdownMenuItem onClick={() => startProjectDownload(undefined, { allVersions: true })}>
All versions
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{canEdit && (
<Button variant="outline" size="sm" asChild>
@@ -398,19 +417,33 @@ export function ProjectContentClient({
Cancel
</Button>
{canDownloadProject && (
<Button
variant="outline"
size="sm"
onClick={() => startProjectDownload(selectedVideoIds)}
disabled={isDownloading || selectedCount === 0}
>
{isDownloading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Download selected
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={isDownloading || selectedCount === 0}
>
{isDownloading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Download selected
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => startProjectDownload(selectedVideoIds)}>
Latest version only
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => startProjectDownload(selectedVideoIds, { allVersions: true })}
>
All versions
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{canEdit && (
<Button
@@ -22,6 +22,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
const session = await auth();
const { projectId } = await params;
const requestedVideoIds = parseRequestedVideoIds(request.nextUrl.searchParams.get('videoIds'));
const includeAllVersions = request.nextUrl.searchParams.get('versions') === 'all';
if (requestedVideoIds && requestedVideoIds.length === 0) {
return apiErrors.badRequest('At least one video must be selected for download');
@@ -92,7 +93,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
}
}
const manifest = buildProjectDownloadManifest(project.name, videos);
const manifest = buildProjectDownloadManifest(project.name, videos, { includeAllVersions });
const validationError = validateProjectDownloadManifest(manifest);
if (validationError) {
return apiErrors.badRequest(validationError);
@@ -315,8 +315,13 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
return apiErrors.badRequest('Download is currently supported for Bunny versions only');
}
if (sourceParam !== null && sourceParam !== 'original' && sourceParam !== 'compressed') {
return apiErrors.badRequest('Invalid source. Allowed values: original, compressed');
if (
sourceParam !== null &&
sourceParam !== 'auto' &&
sourceParam !== 'original' &&
sourceParam !== 'compressed'
) {
return apiErrors.badRequest('Invalid source. Allowed values: auto, original, compressed');
}
if (