Skip to content

Commit 4377278

Browse files
committed
feat: implement thumbnail restoration functionality and enhance video upload process
1 parent 35efc85 commit 4377278

File tree

6 files changed

+109
-10
lines changed

6 files changed

+109
-10
lines changed

next.config.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ const nextConfig: NextConfig = {
44
/* config options here */
55
images: {
66
remotePatterns: [
7-
{
8-
protocol: "https",
9-
hostname: "image.mux.com",
10-
},
117
{
128
protocol: "https",
139
hostname: "stream.mux.com",

src/app/api/uploadthing/core.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { users, videos } from "@/db/schema";
33
import { auth } from "@clerk/nextjs/server";
44
import { and, eq } from "drizzle-orm";
55
import { createUploadthing, type FileRouter } from "uploadthing/next";
6-
import { UploadThingError } from "uploadthing/server";
6+
import { UploadThingError, UTApi } from "uploadthing/server";
77
import z from "zod";
88

99
const f = createUploadthing();
@@ -23,13 +23,38 @@ export const ourFileRouter = {
2323
// Id del usuario de Clerk
2424
const [user] = await db.select().from(users).where(eq(users.clerkId, clerkUserId));
2525
if (!user) throw new UploadThingError("Unauthorized");
26+
27+
// Revisar el video
28+
const [existingVideo] = await db
29+
.select({
30+
thumbnailKey: videos.thumbnailKey,
31+
})
32+
.from(videos)
33+
.where(and(eq(videos.id, input.videoId), eq(videos.userId, user.id)));
34+
35+
if (!existingVideo) throw new UploadThingError("Video not found");
36+
37+
if (existingVideo.thumbnailKey) {
38+
const utapi = new UTApi();
39+
await utapi.deleteFiles(existingVideo.thumbnailKey);
40+
41+
await db
42+
.update(videos)
43+
.set({
44+
thumbnailUrl: null,
45+
thumbnailKey: null,
46+
})
47+
.where(and(eq(videos.id, input.videoId), eq(videos.userId, user.id)));
48+
}
49+
2650
return { user, ...input };
2751
})
2852
.onUploadComplete(async ({ metadata, file }) => {
2953
await db
3054
.update(videos)
3155
.set({
3256
thumbnailUrl: file.ufsUrl,
57+
thumbnailKey: file.key,
3358
})
3459
.where(and(eq(videos.id, metadata.videoId), eq(videos.userId, metadata.user.id)));
3560

src/app/api/videos/webhook/route.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { eq } from "drizzle-orm";
22
import { headers } from "next/headers";
33

4+
import { UTApi } from "uploadthing/server";
5+
46
import { db } from "@/db";
57
import { videos } from "@/db/schema";
68
import { mux } from "@/lib/mux";
@@ -71,18 +73,33 @@ export const POST = async (request: NextRequest) => {
7173
if (!playbackId) {
7274
return new Response("Missing playback ID ", { status: 400 });
7375
}
74-
const thumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.png`;
75-
const previewUrl = `https://image.mux.com/${playbackId}/animated.gif`;
76+
const tempThumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.png`;
77+
const tempPreviewUrl = `https://image.mux.com/${playbackId}/animated.gif`;
7678
const duration = data.duration ? Math.round(data.duration * 1000) : 0;
7779

80+
const utapi = new UTApi();
81+
const [uploadedThumbnail, uploadedPreview] = await utapi.uploadFilesFromUrl([
82+
tempThumbnailUrl,
83+
tempPreviewUrl,
84+
]);
85+
86+
if (!uploadedThumbnail.data || !uploadedPreview.data) {
87+
return new Response("Failed to upload thumbnails", { status: 500 });
88+
}
89+
90+
const { key: thumbnailKey, ufsUrl: thumbnailUrl } = uploadedThumbnail.data;
91+
const { key: previewKey, ufsUrl: previewUrl } = uploadedPreview.data;
92+
7893
await db
7994
.update(videos)
8095
.set({
8196
muxStatus: data.status,
8297
muxPlaybackId: playbackId,
8398
muxAssetId: data.id,
8499
thumbnailUrl,
100+
thumbnailKey,
85101
previewUrl,
102+
previewKey,
86103
duration,
87104
})
88105
.where(eq(videos.muxUploadId, data.upload_id));

src/db/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export const videos = pgTable("videos", {
5050
muxTrackId: text("mux_track_id").unique(),
5151
muxTrackStatus: text("mux_track_status"),
5252
thumbnailUrl: text("thumbnail_url"),
53+
thumbnailKey: text("thumbnail_key"),
5354
previewUrl: text("preview_url"),
55+
previewKey: text("preview_key"),
5456
duration: integer("duration").default(0).notNull(),
5557
visibility: videoVisibility("visibility").notNull().default("private"),
5658
userId: uuid("user_id")

src/modules/studio/ui/sections/form-section.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ import {
4343
TrashIcon,
4444
} from "lucide-react";
4545

46+
import { THUMBNAIL_FALLBACK } from "@/modules/videos/constants";
47+
import Image from "next/image";
4648
import Link from "next/link";
4749
import { useRouter } from "next/navigation";
48-
import Image from "next/image";
4950
import { FC, Suspense, useState } from "react";
5051
import { ErrorBoundary } from "react-error-boundary";
5152
import { useForm } from "react-hook-form";
5253
import { toast } from "sonner";
5354
import { z } from "zod";
54-
import { THUMBNAIL_FALLBACK } from "@/modules/videos/constants";
5555
import { ThumbnailUploadModal } from "../components/thumbnail-upload-modal";
5656

5757
interface FormSectionProps {
@@ -119,6 +119,24 @@ const FormSectionSuspense: FC<FormSectionProps> = ({ videoId }) => {
119119
})
120120
);
121121

122+
const restoreThumbnail = useMutation(
123+
trpc.videos.restoreThumbnail.mutationOptions({
124+
onSuccess: () => {
125+
queryClient.invalidateQueries({
126+
queryKey: trpc.studio.getMany.queryKey(),
127+
});
128+
queryClient.invalidateQueries({
129+
queryKey: trpc.studio.getOne.queryKey({ id: videoId }),
130+
});
131+
toast.success("Thumbnail restored successfully");
132+
},
133+
134+
onError: () => {
135+
toast.error("Something went wrong");
136+
},
137+
})
138+
);
139+
122140
const form = useForm<z.infer<typeof videoUpdateSchema>>({
123141
resolver: zodResolver(videoUpdateSchema),
124142
defaultValues: video,
@@ -242,7 +260,9 @@ const FormSectionSuspense: FC<FormSectionProps> = ({ videoId }) => {
242260
<SparklesIcon className="size-4 mr-" />
243261
AI-generate
244262
</DropdownMenuItem>
245-
<DropdownMenuItem>
263+
<DropdownMenuItem
264+
onClick={() => restoreThumbnail.mutate({ id: videoId })}
265+
>
246266
<RotateCcwIcon className="size-4 mr-" />
247267
Restore
248268
</DropdownMenuItem>

src/modules/videos/server/procedures.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,48 @@ import { mux } from "@/lib/mux";
44
import { createTRPCRouter, protectedProcedure } from "@/trpc/init";
55
import { TRPCError } from "@trpc/server";
66
import { and, eq } from "drizzle-orm";
7+
import { UTApi } from "uploadthing/server";
78
import z from "zod";
89

910
export const videosRouter = createTRPCRouter({
11+
restoreThumbnail: protectedProcedure
12+
.input(z.object({ id: z.uuid() }))
13+
.mutation(async ({ ctx, input }) => {
14+
const { id: userId } = ctx.user;
15+
16+
const [existingVideo] = await db
17+
.select()
18+
.from(videos)
19+
.where(and(eq(videos.id, input.id), eq(videos.userId, userId)));
20+
21+
if (!existingVideo) {
22+
throw new TRPCError({ code: "NOT_FOUND" });
23+
}
24+
25+
if (!existingVideo.muxPlaybackId) {
26+
throw new TRPCError({ code: "BAD_REQUEST" });
27+
}
28+
29+
const tempthumbnailUrl = `https://image.mux.com/${existingVideo.muxPlaybackId}/thumbnail.png`;
30+
31+
const utapi = new UTApi();
32+
const uploadedThumbnail = await utapi.uploadFilesFromUrl(tempthumbnailUrl);
33+
34+
if (!uploadedThumbnail.data) {
35+
throw new TRPCError({ code: "BAD_REQUEST" });
36+
}
37+
38+
const { key: thumbnailKey, ufsUrl: thumbnailUrl } = uploadedThumbnail.data;
39+
40+
const [updatedVideo] = await db
41+
.update(videos)
42+
.set({ thumbnailUrl, thumbnailKey })
43+
.where(and(eq(videos.id, input.id), eq(videos.userId, userId)))
44+
.returning();
45+
46+
return updatedVideo;
47+
}),
48+
1049
remove: protectedProcedure.input(z.object({ id: z.uuid() })).mutation(async ({ ctx, input }) => {
1150
const { id: userId } = ctx.user;
1251

0 commit comments

Comments
 (0)