mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
281 lines
7.7 KiB
TypeScript
281 lines
7.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { webEnv } from "@opencut/env/web";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
|
|
const searchParamsSchema = z.object({
|
|
q: z.string().max(500, "Query too long").optional(),
|
|
type: z.enum(["songs", "effects"]).optional(),
|
|
page: z.coerce.number().int().min(1).max(1000).default(1),
|
|
page_size: z.coerce.number().int().min(1).max(150).default(20),
|
|
sort: z
|
|
.enum(["downloads", "rating", "created", "score"])
|
|
.default("downloads"),
|
|
min_rating: z.coerce.number().min(0).max(5).default(3),
|
|
commercial_only: z.coerce.boolean().default(true),
|
|
});
|
|
|
|
const freesoundResultSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
description: z.string(),
|
|
url: z.string().url(),
|
|
previews: z
|
|
.object({
|
|
"preview-hq-mp3": z.string().url(),
|
|
"preview-lq-mp3": z.string().url(),
|
|
"preview-hq-ogg": z.string().url(),
|
|
"preview-lq-ogg": z.string().url(),
|
|
})
|
|
.optional(),
|
|
download: z.string().url().optional(),
|
|
duration: z.number(),
|
|
filesize: z.number(),
|
|
type: z.string(),
|
|
channels: z.number(),
|
|
bitrate: z.number(),
|
|
bitdepth: z.number(),
|
|
samplerate: z.number(),
|
|
username: z.string(),
|
|
tags: z.array(z.string()),
|
|
license: z.string(),
|
|
created: z.string(),
|
|
num_downloads: z.number().optional(),
|
|
avg_rating: z.number().optional(),
|
|
num_ratings: z.number().optional(),
|
|
});
|
|
|
|
const freesoundResponseSchema = z.object({
|
|
count: z.number(),
|
|
next: z.string().url().nullable(),
|
|
previous: z.string().url().nullable(),
|
|
results: z.array(freesoundResultSchema),
|
|
});
|
|
|
|
const transformedResultSchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
description: z.string(),
|
|
url: z.string(),
|
|
previewUrl: z.string().optional(),
|
|
downloadUrl: z.string().optional(),
|
|
duration: z.number(),
|
|
filesize: z.number(),
|
|
type: z.string(),
|
|
channels: z.number(),
|
|
bitrate: z.number(),
|
|
bitdepth: z.number(),
|
|
samplerate: z.number(),
|
|
username: z.string(),
|
|
tags: z.array(z.string()),
|
|
license: z.string(),
|
|
created: z.string(),
|
|
downloads: z.number().optional(),
|
|
rating: z.number().optional(),
|
|
ratingCount: z.number().optional(),
|
|
});
|
|
|
|
const apiResponseSchema = z.object({
|
|
count: z.number(),
|
|
next: z.string().nullable(),
|
|
previous: z.string().nullable(),
|
|
results: z.array(transformedResultSchema),
|
|
query: z.string().optional(),
|
|
type: z.string(),
|
|
page: z.number(),
|
|
pageSize: z.number(),
|
|
sort: z.string(),
|
|
minRating: z.number().optional(),
|
|
});
|
|
|
|
function buildSortParameter({ query, sort }: { query?: string; sort: string }) {
|
|
if (!query) return `${sort}_desc`;
|
|
return sort === "score" ? "score" : `${sort}_desc`;
|
|
}
|
|
|
|
function applyEffectsFilters({
|
|
params,
|
|
min_rating,
|
|
commercial_only,
|
|
}: {
|
|
params: URLSearchParams;
|
|
min_rating: number;
|
|
commercial_only: boolean;
|
|
}) {
|
|
params.append("filter", "duration:[* TO 30.0]");
|
|
params.append("filter", `avg_rating:[${min_rating} TO *]`);
|
|
|
|
if (commercial_only) {
|
|
params.append(
|
|
"filter",
|
|
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")',
|
|
);
|
|
}
|
|
|
|
params.append(
|
|
"filter",
|
|
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion",
|
|
);
|
|
}
|
|
|
|
function transformFreesoundResult(
|
|
result: z.infer<typeof freesoundResultSchema>,
|
|
) {
|
|
return {
|
|
id: result.id,
|
|
name: result.name,
|
|
description: result.description,
|
|
url: result.url,
|
|
previewUrl:
|
|
result.previews?.["preview-hq-mp3"] ||
|
|
result.previews?.["preview-lq-mp3"],
|
|
downloadUrl: result.download,
|
|
duration: result.duration,
|
|
filesize: result.filesize,
|
|
type: result.type,
|
|
channels: result.channels,
|
|
bitrate: result.bitrate,
|
|
bitdepth: result.bitdepth,
|
|
samplerate: result.samplerate,
|
|
username: result.username,
|
|
tags: result.tags,
|
|
license: result.license,
|
|
created: result.created,
|
|
downloads: result.num_downloads || 0,
|
|
rating: result.avg_rating || 0,
|
|
ratingCount: result.num_ratings || 0,
|
|
};
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { limited } = await checkRateLimit({ request });
|
|
if (limited) {
|
|
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
}
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
|
|
const validationResult = searchParamsSchema.safeParse({
|
|
q: searchParams.get("q") || undefined,
|
|
type: searchParams.get("type") || undefined,
|
|
page: searchParams.get("page") || undefined,
|
|
page_size: searchParams.get("page_size") || undefined,
|
|
sort: searchParams.get("sort") || undefined,
|
|
min_rating: searchParams.get("min_rating") || undefined,
|
|
});
|
|
|
|
if (!validationResult.success) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "Invalid parameters",
|
|
details: validationResult.error.flatten().fieldErrors,
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const {
|
|
q: query,
|
|
type,
|
|
page,
|
|
page_size: pageSize,
|
|
sort,
|
|
min_rating,
|
|
commercial_only,
|
|
} = validationResult.data;
|
|
|
|
if (type === "songs") {
|
|
return NextResponse.json(
|
|
{
|
|
error: "Songs are not available yet",
|
|
message:
|
|
"Song search functionality is coming soon. Try searching for sound effects instead.",
|
|
},
|
|
{ status: 501 },
|
|
);
|
|
}
|
|
|
|
const baseUrl = "https://freesound.org/apiv2/search/text/";
|
|
|
|
const sortParam = buildSortParameter({ query, sort });
|
|
|
|
const params = new URLSearchParams({
|
|
query: query || "",
|
|
token: webEnv.FREESOUND_API_KEY,
|
|
page: page.toString(),
|
|
page_size: pageSize.toString(),
|
|
sort: sortParam,
|
|
fields:
|
|
"id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
|
|
});
|
|
|
|
const isEffectsSearch = type === "effects" || !type;
|
|
if (isEffectsSearch) {
|
|
applyEffectsFilters({ params, min_rating, commercial_only });
|
|
}
|
|
|
|
const response = await fetch(`${baseUrl}?${params.toString()}`);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error("Freesound API error:", response.status, errorText);
|
|
return NextResponse.json(
|
|
{ error: "Failed to search sounds" },
|
|
{ status: response.status },
|
|
);
|
|
}
|
|
|
|
const rawData = await response.json();
|
|
|
|
const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
|
|
if (!freesoundValidation.success) {
|
|
console.error(
|
|
"Invalid Freesound API response:",
|
|
freesoundValidation.error,
|
|
);
|
|
return NextResponse.json(
|
|
{ error: "Invalid response from Freesound API" },
|
|
{ status: 502 },
|
|
);
|
|
}
|
|
|
|
const data = freesoundValidation.data;
|
|
|
|
const transformedResults = data.results.map(transformFreesoundResult);
|
|
|
|
const responseData = {
|
|
count: data.count,
|
|
next: data.next,
|
|
previous: data.previous,
|
|
results: transformedResults,
|
|
query: query || "",
|
|
type: type || "effects",
|
|
page,
|
|
pageSize,
|
|
sort,
|
|
minRating: min_rating,
|
|
};
|
|
|
|
const responseValidation = apiResponseSchema.safeParse(responseData);
|
|
if (!responseValidation.success) {
|
|
console.error(
|
|
"Invalid API response structure:",
|
|
responseValidation.error,
|
|
);
|
|
return NextResponse.json(
|
|
{ error: "Internal response formatting error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(responseValidation.data);
|
|
} catch (error) {
|
|
console.error("Error searching sounds:", error);
|
|
return NextResponse.json(
|
|
{ error: "Internal server error" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|