feat: enhance canvas functionality with scissors mode and node template updates
- 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.
This commit is contained in:
164
convex/pexels.ts
Normal file
164
convex/pexels.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
"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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user