- Implemented visual feedback and cursor changes for scissors mode in dark and light themes, improving user interaction during edge manipulation. - Updated node template picker to include new keywords for AI image generation, enhancing searchability. - Renamed and categorized node types for clarity, including updates to asset and prompt nodes. - Added support for video nodes and adjusted related components for improved media handling on the canvas.
165 lines
4.1 KiB
TypeScript
165 lines
4.1 KiB
TypeScript
"use node";
|
|
|
|
import { v } from "convex/values";
|
|
import { action } from "./_generated/server";
|
|
|
|
/** Canonical API base (legacy /videos/ ohne /v1/ ist deprecated laut Pexels-Doku). */
|
|
const PEXELS_VIDEO_API = "https://api.pexels.com/v1/videos";
|
|
|
|
interface PexelsVideoFile {
|
|
id: number;
|
|
quality: "hd" | "sd" | "uhd" | "hls";
|
|
file_type: string;
|
|
width: number;
|
|
height: number;
|
|
fps: number;
|
|
link: string;
|
|
}
|
|
|
|
interface PexelsVideo {
|
|
id: number;
|
|
width: number;
|
|
height: number;
|
|
url: string;
|
|
image: string;
|
|
duration: number;
|
|
user: { id: number; name: string; url: string };
|
|
video_files: PexelsVideoFile[];
|
|
}
|
|
|
|
function pickPlayableVideoFile(files: PexelsVideoFile[]): PexelsVideoFile {
|
|
const playable = files.filter((f) => {
|
|
if (f.quality === "hls") return false;
|
|
const url = f.link.toLowerCase();
|
|
if (url.includes(".m3u8")) return false;
|
|
return url.includes(".mp4");
|
|
});
|
|
if (playable.length === 0) {
|
|
throw new Error("No progressive MP4 in Pexels video_files");
|
|
}
|
|
return (
|
|
playable.find((f) => f.quality === "hd") ??
|
|
playable.find((f) => f.quality === "uhd") ??
|
|
playable.find((f) => f.quality === "sd") ??
|
|
playable[0]
|
|
);
|
|
}
|
|
|
|
/** Frische MP4-URL (Signing kann ablaufen) — gleiche Auswahl wie beim ersten Pick. */
|
|
export const getVideoByPexelsId = action({
|
|
args: { pexelsId: v.number() },
|
|
handler: async (_ctx, { pexelsId }) => {
|
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
if (!apiKey) {
|
|
throw new Error("PEXELS_API_KEY not set");
|
|
}
|
|
|
|
const res = await fetch(`${PEXELS_VIDEO_API}/${pexelsId}`, {
|
|
headers: { Authorization: apiKey },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Pexels API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
|
|
const video = (await res.json()) as PexelsVideo;
|
|
const file = pickPlayableVideoFile(video.video_files);
|
|
return {
|
|
mp4Url: file.link,
|
|
width: video.width,
|
|
height: video.height,
|
|
duration: video.duration,
|
|
};
|
|
},
|
|
});
|
|
|
|
export const searchVideos = action({
|
|
args: {
|
|
query: v.string(),
|
|
orientation: v.optional(
|
|
v.union(
|
|
v.literal("landscape"),
|
|
v.literal("portrait"),
|
|
v.literal("square"),
|
|
),
|
|
),
|
|
minDuration: v.optional(v.number()),
|
|
maxDuration: v.optional(v.number()),
|
|
page: v.optional(v.number()),
|
|
perPage: v.optional(v.number()),
|
|
},
|
|
handler: async (_ctx, args) => {
|
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
if (!apiKey) {
|
|
throw new Error("PEXELS_API_KEY not set");
|
|
}
|
|
|
|
const params = new URLSearchParams({
|
|
query: args.query,
|
|
per_page: String(args.perPage ?? 20),
|
|
page: String(args.page ?? 1),
|
|
...(args.orientation && { orientation: args.orientation }),
|
|
...(args.minDuration != null && {
|
|
min_duration: String(args.minDuration),
|
|
}),
|
|
...(args.maxDuration != null && {
|
|
max_duration: String(args.maxDuration),
|
|
}),
|
|
});
|
|
|
|
const res = await fetch(`${PEXELS_VIDEO_API}/search?${params}`, {
|
|
headers: { Authorization: apiKey },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Pexels API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
|
|
const data = (await res.json()) as {
|
|
videos: PexelsVideo[];
|
|
total_results: number;
|
|
next_page?: string;
|
|
page?: number;
|
|
per_page?: number;
|
|
};
|
|
|
|
return data;
|
|
},
|
|
});
|
|
|
|
export const popularVideos = action({
|
|
args: {
|
|
page: v.optional(v.number()),
|
|
perPage: v.optional(v.number()),
|
|
},
|
|
handler: async (_ctx, args) => {
|
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
if (!apiKey) {
|
|
throw new Error("PEXELS_API_KEY not set");
|
|
}
|
|
|
|
const params = new URLSearchParams({
|
|
per_page: String(args.perPage ?? 20),
|
|
page: String(args.page ?? 1),
|
|
});
|
|
|
|
const res = await fetch(`${PEXELS_VIDEO_API}/popular?${params}`, {
|
|
headers: { Authorization: apiKey },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`Pexels API error: ${res.status} ${res.statusText}`);
|
|
}
|
|
|
|
const data = (await res.json()) as {
|
|
videos: PexelsVideo[];
|
|
total_results?: number;
|
|
next_page?: string;
|
|
page?: number;
|
|
per_page?: number;
|
|
};
|
|
|
|
return data;
|
|
},
|
|
});
|