Skip to content

Commit

Permalink
feat: record yt uploads in db
Browse files Browse the repository at this point in the history
- show yt in the single generation page
  • Loading branch information
listlessbird committed Dec 10, 2024
1 parent d337cb9 commit 7277676
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 76 deletions.
43 changes: 41 additions & 2 deletions web/src/app/(history)/_components/generation-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ItemProgressIndicator } from "@/app/(history)/_components/item-progress
import {
useGenerationProgress,
useGenerationQuery,
useUploadedVideos,
} from "@/app/(history)/history/(item)/[id]/queries";
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
Expand All @@ -18,6 +19,9 @@ import { cn } from "@/lib/utils";
import { Generate } from "@/app/(history)/history/(item)/[id]/generate";
import { createVideoScriptAction } from "@/app/(main)/action";
import { UploadToYouTube } from "@/app/(history)/_components/upload-to-yt";
import { Badge } from "@/components/ui/badge";
import { FaYoutube } from "react-icons/fa6";
import Link from "next/link";
export function GenerationViewer({ generationId }: { generationId: string }) {
const [isRetrying, setIsRetrying] = useState(false);

Expand All @@ -29,6 +33,11 @@ export function GenerationViewer({ generationId }: { generationId: string }) {

const { messages, status } = useGenerationProgress(generationId);

const { data: uploadedVideo } = useUploadedVideos(
generationId,
generation?.status === "complete" || false
);

const [activeTab, setActiveTab] = useState(() =>
generation?.status === "complete" ? "content" : "progress"
);
Expand Down Expand Up @@ -88,7 +97,9 @@ export function GenerationViewer({ generationId }: { generationId: string }) {
<div className="container max-w-7xl mx-auto py-8 space-y-8">
<div className="flex items-center justify-between">
<h1 className="text-4xl font-bold capitalize">{topic}</h1>
<Progress value={progress} className="w-32" />
{generation?.status !== "complete" && (
<Progress value={progress} className="w-32" />
)}
</div>

<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
Expand Down Expand Up @@ -215,12 +226,40 @@ export function GenerationViewer({ generationId }: { generationId: string }) {
>
<source src={videoUrl!} type="video/mp4" />
</video>
<UploadToYouTube defaultTitle={topic!} videoUrl={videoUrl!} />
<div className="my-2">
{!uploadedVideo?.videoUrl && (
<UploadToYouTube
defaultTitle={topic!}
videoUrl={videoUrl!}
generationId={generationId}
/>
)}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>

{uploadedVideo && (
<Card>
<CardContent className="p-6">
<Link
className="contents"
href={uploadedVideo.videoUrl}
target="_blank"
>
<Badge className="py-2">
<FaYoutube className="mr-2 h-4 w-4" />
<span className="text-sm mr-2">/</span>
<p className="text-sm lowercase">
{uploadedVideo.videoUrl.split("/").pop()}
</p>
</Badge>
</Link>
</CardContent>
</Card>
)}

{!videoUrl && generation?.status === "complete" && (
<Generate asset={generation} />
)}
Expand Down
10 changes: 10 additions & 0 deletions web/src/app/(history)/_components/upload-to-yt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { uploadToYTAction } from "@/lib/yt/yt-upload.action";
import { Youtube, Loader2 } from "lucide-react";
import { YoutubeConnectionStatus } from "@/app/(main)/settings/yt-conn-status";
import { Card, CardContent } from "@/components/ui/card";

export function UploadToYouTube({
videoUrl,
defaultTitle,
generationId,
}: {
videoUrl: string;
defaultTitle: string;
generationId: string;
}) {
const [open, setOpen] = useState(false);
const [step, setStep] = useState<"details" | "uploading" | "complete">(
Expand All @@ -39,6 +43,7 @@ export function UploadToYouTube({
videoUrl,
title,
description,
generationId,
});
setStep("complete");
toast({
Expand Down Expand Up @@ -74,6 +79,11 @@ export function UploadToYouTube({
"Your video has been uploaded successfully!"}
</DialogDescription>
</DialogHeader>
<Card>
<CardContent className="p-2">
<YoutubeConnectionStatus />
</CardContent>
</Card>
{step === "details" && (
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
Expand Down
12 changes: 11 additions & 1 deletion web/src/app/(history)/history/(item)/[id]/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getGenerationAction } from "@/app/(history)/history/(item)/[id]/get-generation-action";
import { Generation } from "@/db/schema";
import { ProgressUpdate } from "@/lib/send-progress";
import { getUploaded } from "@/lib/yt/yt-upload.action";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";

Expand All @@ -9,11 +10,20 @@ export function useGenerationQuery(id: string) {
queryKey: ["generation-item", id],
queryFn: () => getGenerationAction(id),
refetchInterval: (query) => {
return query.state.data?.status === "complete" ? false : 5 * 1000;
return query.state.data?.status === "complete" ? false : 10 * 1000;
},
});
}

export function useUploadedVideos(configId: string, isComplete: boolean) {
return useQuery({
queryKey: ["uploaded-videos", configId],
queryFn: () => getUploaded(configId),
refetchInterval: 5 * 1000,
enabled: isComplete,
});
}

export function useGenerationProgress(id: string) {
const [messages, setMessages] = useState<ProgressUpdate[]>([]);
const [status, setStatue] = useState("pending");
Expand Down
71 changes: 2 additions & 69 deletions web/src/app/(main)/settings/settings-form.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,15 @@
"use client";

import { useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
disconnectYoutube,
getYoutubeStatus,
startYoutubeFlow,
} from "@/app/(main)/settings/action";
import { Youtube } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "@/hooks/use-toast";
import { YoutubeConnectionStatus } from "@/app/(main)/settings/yt-conn-status";

export function SettingsForm() {
const searchParams = useSearchParams();
const qc = useQueryClient();
// @ts-ignore
const justConnected = searchParams.get("yt_connected") === true;

const { data: ytStatus, isLoading } = useQuery({
queryKey: ["youtube-status"],
queryFn: getYoutubeStatus,
initialData: justConnected ? { connected: true } : undefined,
});

const disconnectMutation = useMutation({
mutationFn: disconnectYoutube,
onSuccess: () => {
qc.setQueryData(["youtube-status"], { connected: false });
},
onError: (error) => {
toast({
title: "Error disconnecting YouTube",
description: error.message,
variant: "destructive",
});
},
});

const handleYoutubeConnection = async () => {
if (ytStatus?.connected) {
await disconnectMutation.mutateAsync();
} else {
await startYoutubeFlow();
}
};

return (
<div className="grid gap-6 md:grid-cols-2">
<Card className="w-full">
Expand All @@ -63,31 +20,7 @@ export function SettingsForm() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Youtube className="size-6 text-red-600" />
{isLoading ? (
<span className="text-muted-foreground">
Checking status...
</span>
) : (
<span>
{ytStatus?.connected ? "Connected" : "Not connected"}
</span>
)}
</div>
<Button
variant={ytStatus?.connected ? "destructive" : "default"}
onClick={handleYoutubeConnection}
disabled={isLoading || disconnectMutation.isPending}
>
{disconnectMutation.isPending
? "Disconnecting..."
: ytStatus?.connected
? "Disconnect"
: "Connect"}
</Button>
</div>
<YoutubeConnectionStatus />
</CardContent>
</Card>
</div>
Expand Down
72 changes: 72 additions & 0 deletions web/src/app/(main)/settings/yt-conn-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import { Button } from "@/components/ui/button";
import {
disconnectYoutube,
getYoutubeStatus,
startYoutubeFlow,
} from "@/app/(main)/settings/action";
import { Youtube } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "@/hooks/use-toast";

export function YoutubeConnectionStatus() {
const searchParams = useSearchParams();
const qc = useQueryClient();

// @ts-ignore
const justConnected = searchParams.get("yt_connected") === true;

const { data: ytStatus, isLoading } = useQuery({
queryKey: ["youtube-status"],
queryFn: getYoutubeStatus,
initialData: justConnected ? { connected: true } : undefined,
});

const disconnectMutation = useMutation({
mutationFn: disconnectYoutube,
onSuccess: () => {
qc.setQueryData(["youtube-status"], { connected: false });
},
onError: (error) => {
toast({
title: "Error disconnecting YouTube",
description: error.message,
variant: "destructive",
});
},
});

const handleYoutubeConnection = async () => {
if (ytStatus?.connected) {
await disconnectMutation.mutateAsync();
} else {
await startYoutubeFlow();
}
};
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Youtube className="size-6 text-red-600" />
{isLoading ? (
<span className="text-muted-foreground">Checking status...</span>
) : (
<span>{ytStatus?.connected ? "Connected" : "Not connected"}</span>
)}
</div>
<Button
variant={ytStatus?.connected ? "destructive" : "default"}
onClick={handleYoutubeConnection}
disabled={isLoading || disconnectMutation.isPending}
className="h-8"
>
{disconnectMutation.isPending
? "Disconnecting..."
: ytStatus?.connected
? "Disconnect"
: "Connect"}
</Button>
</div>
);
}
28 changes: 26 additions & 2 deletions web/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import {
text,
timestamp,
integer,
varchar,
uuid,
pgEnum,
serial,
jsonb,
} from "drizzle-orm/pg-core";
import { type InferSelectModel } from "drizzle-orm";
Expand Down Expand Up @@ -109,6 +107,32 @@ export const youtubeCredentialsTable = pgTable("yt_credentials", {
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

export const uploadedVideosTable = pgTable("uploaded_videos", {
id: text("id").primaryKey(), // YouTube video ID
userId: text("user_id")
.notNull()
.references(() => userTable.googleId, { onDelete: "cascade" }),
configId: text("config_id").references(() => configTable.configId, {
onDelete: "set null",
}),
title: text("title").notNull(),
description: text("description"),
videoUrl: text("video_url").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
export const uploadedVideosRelations = relations(
uploadedVideosTable,
({ one }) => ({
user: one(userTable, {
fields: [uploadedVideosTable.userId],
references: [userTable.googleId],
}),
config: one(configTable, {
fields: [uploadedVideosTable.configId],
references: [configTable.configId],
}),
})
);
export const youtubeCredentialsRelations = relations(
youtubeCredentialsTable,
({ one }) => ({
Expand Down
21 changes: 21 additions & 0 deletions web/src/db/yt-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,24 @@ export async function getYtCredentialsFromDb(userId: string) {
return null;
}
}

export async function getUploadedVideosFromDb(configId: string) {
try {
const uploaded = await db.query.uploadedVideosTable.findFirst({
where: (fields, operators) => {
return operators.eq(fields.configId, configId);
},
});

if (!uploaded) return null;

return {
title: uploaded.title,
description: uploaded.description,
videoUrl: uploaded.videoUrl,
};
} catch (error) {
console.error("Error finding uploaded videos from db:", error);
return null;
}
}
Loading

0 comments on commit 7277676

Please sign in to comment.