From 2c2875298519e1aadc034835ac5056935b653e70 Mon Sep 17 00:00:00 2001 From: kraj2503 <73212876+kraj2503@users.noreply.github.com> Date: Tue, 24 Dec 2024 01:36:02 +0530 Subject: [PATCH] feat: remove dependency on 3rd-party YouTube API for thumbnails and titles --- next-app/app/api/streams/route.ts | 119 ++++---- next-app/components/OldStreamView.tsx | 277 ++++++++++-------- .../components/StreamView/AddSongForm.tsx | 67 +++-- next-app/next.config.mjs | 2 +- next-app/package.json | 3 +- package-lock.json | 6 + 6 files changed, 254 insertions(+), 220 deletions(-) create mode 100644 package-lock.json diff --git a/next-app/app/api/streams/route.ts b/next-app/app/api/streams/route.ts index 6914077..65850b5 100644 --- a/next-app/app/api/streams/route.ts +++ b/next-app/app/api/streams/route.ts @@ -1,17 +1,15 @@ import { z } from "zod"; import { NextRequest, NextResponse } from "next/server"; import db from "@/lib/db"; -//@ts-ignore -import youtubesearchapi from "youtube-search-api"; import { YT_REGEX } from "@/lib/utils"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth-options"; - +import axios from "axios"; const CreateStreamSchema = z.object({ creatorId: z.string(), url: z.string(), - spaceId:z.string() + spaceId: z.string(), }); const MAX_QUEUE_LEN = 20; @@ -58,7 +56,9 @@ export async function POST(req: NextRequest) { ); } - const res = await youtubesearchapi.GetVideoDetails(videoId); + const res = await axios.get( + `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`, + ); // Check if the user is not the creator if (user.id !== data.creatorId) { @@ -132,11 +132,6 @@ export async function POST(req: NextRequest) { } } - const thumbnails = res.thumbnail.thumbnails; - thumbnails.sort((a: { width: number }, b: { width: number }) => - a.width < b.width ? -1 : 1, - ); - const existingActiveStreams = await db.stream.count({ where: { spaceId: data.spaceId, @@ -162,16 +157,10 @@ export async function POST(req: NextRequest) { url: data.url, extractedId: videoId, type: "Youtube", - title: res.title ?? "Can't find video", - smallImg: - (thumbnails.length > 1 - ? thumbnails[thumbnails.length - 2].url - : thumbnails[thumbnails.length - 1].url) ?? - "https://cdn.pixabay.com/photo/2024/02/28/07/42/european-shorthair-8601492_640.jpg", - bigImg: - thumbnails[thumbnails.length - 1].url ?? - "https://cdn.pixabay.com/photo/2024/02/28/07/42/european-shorthair-8601492_640.jpg", - spaceId:data.spaceId + title: res.data.title ?? "Can't find video", + smallImg: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`, + bigImg: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, + spaceId: data.spaceId, }, }); @@ -209,68 +198,68 @@ export async function GET(req: NextRequest) { const user = session.user; if (!spaceId) { - return NextResponse.json({ - message: "Error" - }, { - status: 411 - }) -} + return NextResponse.json( + { + message: "Error", + }, + { + status: 411, + }, + ); + } const [space, activeStream] = await Promise.all([ db.space.findUnique({ where: { - id: spaceId, + id: spaceId, }, include: { - streams: { - include: { - _count: { - select: { - upvotes: true - } - }, - upvotes: { - where: { - userId: session?.user.id - } - } - + streams: { + include: { + _count: { + select: { + upvotes: true, }, - where:{ - played:false - } + }, + upvotes: { + where: { + userId: session?.user.id, + }, + }, }, - _count: { - select: { - streams: true - } - }, - - } - - }), - db.currentStream.findFirst({ + where: { + played: false, + }, + }, + _count: { + select: { + streams: true, + }, + }, + }, + }), + db.currentStream.findFirst({ where: { - spaceId: spaceId + spaceId: spaceId, }, include: { - stream: true - } - }) + stream: true, + }, + }), ]); - const hostId =space?.hostId; - const isCreator = session.user.id=== hostId + const hostId = space?.hostId; + const isCreator = session.user.id === hostId; return NextResponse.json({ - streams: space?.streams.map(({_count, ...rest}) => ({ - ...rest, - upvotes: _count.upvotes, - haveUpvoted: rest.upvotes.length ? true : false + streams: space?.streams.map(({ _count, ...rest }) => ({ + ...rest, + upvotes: _count.upvotes, + haveUpvoted: rest.upvotes.length ? true : false, })), activeStream, hostId, isCreator, - spaceName:space?.name -}); + spaceName: space?.name, + }); } diff --git a/next-app/components/OldStreamView.tsx b/next-app/components/OldStreamView.tsx index c42a19f..807608a 100644 --- a/next-app/components/OldStreamView.tsx +++ b/next-app/components/OldStreamView.tsx @@ -3,7 +3,17 @@ import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; -import { ChevronUp, ChevronDown, Share2, Play, Trash2, X, MessageCircle, Instagram, Twitter} from "lucide-react"; +import { + ChevronUp, + ChevronDown, + Share2, + Play, + Trash2, + X, + MessageCircle, + Instagram, + Twitter, +} from "lucide-react"; import { toast } from "sonner"; import { Appbar } from "./Appbar"; import LiteYouTubeEmbed from "react-lite-youtube-embed"; @@ -21,9 +31,14 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog"; -import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; - - +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; interface Video { id: string; @@ -37,7 +52,7 @@ interface Video { userId: string; upvotes: number; haveUpvoted: boolean; - spaceId:string + spaceId: string; } interface CustomSession extends Omit { @@ -54,11 +69,11 @@ const REFRESH_INTERVAL_MS = 10 * 1000; export default function StreamView({ creatorId, playVideo = false, - spaceId + spaceId, }: { creatorId: string; playVideo: boolean; - spaceId:string; + spaceId: string; }) { const [inputLink, setInputLink] = useState(""); const [queue, setQueue] = useState([]); @@ -68,7 +83,7 @@ export default function StreamView({ const videoPlayerRef = useRef(null); const [isCreator, setIsCreator] = useState(false); const [isEmptyQueueDialogOpen, setIsEmptyQueueDialogOpen] = useState(false); - const [spaceName,setSpaceName]=useState("") + const [spaceName, setSpaceName] = useState(""); const [isOpen, setIsOpen] = useState(false); async function refreshStreams() { @@ -94,9 +109,8 @@ export default function StreamView({ return json.activeStream?.stream || null; }); - setIsCreator(json.isCreator); - setSpaceName(json.spaceName) + setSpaceName(json.spaceName); } catch (error) { console.error("Error refreshing streams:", error); setQueue([]); @@ -149,7 +163,7 @@ export default function StreamView({ body: JSON.stringify({ creatorId, url: inputLink, - spaceId:spaceId + spaceId: spaceId, }), }); const data = await res.json(); @@ -189,7 +203,7 @@ export default function StreamView({ method: "POST", body: JSON.stringify({ streamId: id, - spaceId:spaceId + spaceId: spaceId, }), }); }; @@ -212,47 +226,52 @@ export default function StreamView({ } }; - const handleShare = (platform: 'whatsapp' | 'twitter' | 'instagram' | 'clipboard') => { - const shareableLink = `${window.location.hostname}/spaces/${spaceId}` + const handleShare = ( + platform: "whatsapp" | "twitter" | "instagram" | "clipboard", + ) => { + const shareableLink = `${window.location.hostname}/spaces/${spaceId}`; - if (platform === 'clipboard') { - navigator.clipboard.writeText(shareableLink).then(() => { - toast.success('Link copied to clipboard!') - }).catch((err) => { - console.error('Could not copy text: ', err) - toast.error('Failed to copy link. Please try again.') - }) + if (platform === "clipboard") { + navigator.clipboard + .writeText(shareableLink) + .then(() => { + toast.success("Link copied to clipboard!"); + }) + .catch((err) => { + console.error("Could not copy text: ", err); + toast.error("Failed to copy link. Please try again."); + }); } else { - let url + let url; switch (platform) { - case 'whatsapp': - url = `https://wa.me/?text=${encodeURIComponent(shareableLink)}` - break - case 'twitter': - url = `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareableLink)}` - break - case 'instagram': + case "whatsapp": + url = `https://wa.me/?text=${encodeURIComponent(shareableLink)}`; + break; + case "twitter": + url = `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareableLink)}`; + break; + case "instagram": // Instagram doesn't allow direct URL sharing, so we copy the link instead - navigator.clipboard.writeText(shareableLink) - toast.success('Link copied for Instagram sharing!') - return + navigator.clipboard.writeText(shareableLink); + toast.success("Link copied for Instagram sharing!"); + return; default: - return + return; } - window.open(url, '_blank') + window.open(url, "_blank"); } - } + }; const emptyQueue = async () => { try { const res = await fetch("/api/streams/empty-queue", { method: "POST", headers: { - 'Content-Type': 'application/json', - }, + "Content-Type": "application/json", + }, body: JSON.stringify({ - spaceId:spaceId - }) + spaceId: spaceId, + }), }); const data = await res.json(); if (res.ok) { @@ -270,9 +289,12 @@ export default function StreamView({ const removeSong = async (streamId: string) => { try { - const res = await fetch(`/api/streams/remove?streamId=${streamId}&spaceId=${spaceId}`, { - method: "DELETE", - }); + const res = await fetch( + `/api/streams/remove?streamId=${streamId}&spaceId=${spaceId}`, + { + method: "DELETE", + }, + ); if (res.ok) { toast.success("Song removed successfully"); refreshStreams(); @@ -285,64 +307,67 @@ export default function StreamView({ }; return ( -
+
-
- {spaceName} -
+
+ {spaceName} +
-
-
-
-

+
+
+
+

Upcoming Songs

- - - - + + + + - - Share to Social Media - - - handleShare('whatsapp')}> -
- - WhatsApp -
-
- - handleShare('twitter')}> -
- - Twitter -
-
- - handleShare('instagram')}> -
- - Instagram -
-
+ + Share to Social Media + - + handleShare("whatsapp")}> +
+ + WhatsApp +
+
- handleShare('clipboard')}> -
- Copy Link to Clipboard -
-
-
-
+ handleShare("twitter")}> +
+ + Twitter +
+
+ + handleShare("instagram")}> +
+ + Instagram +
+
+ + + + handleShare("clipboard")}> +
+ Copy Link to Clipboard +
+
+ +
{isCreator && ( @@ -350,9 +375,9 @@ export default function StreamView({
{queue.length === 0 ? ( - - -

+ + +

No videos in queue

@@ -362,25 +387,25 @@ export default function StreamView({ {queue.map((video) => ( - + {`Thumbnail
-

+

{video.title}

{video.title} -
+
@@ -418,10 +443,10 @@ export default function StreamView({
)}
-
+
- - + +

Add a song

setInputLink(e.target.value)} - className="bg-gray-700 text-white border-gray-600 placeholder-gray-400" + className="border-gray-600 bg-gray-700 text-white placeholder-gray-400" />
- {inputLink && inputLink.match(YT_REGEX) && !loading && ( -
- -
- )} + {inputLink && + !loading && + (() => { + const match = inputLink.match(YT_REGEX); + if (match) { + const extractedId = match[1]; + return ( +
+ Thumbnail +
+ ); + } + })()}
- - + +

Now Playing

{currentVideo ? (
{playVideo ? (
) : ( <> {currentVideo.title}

@@ -473,7 +508,7 @@ export default function StreamView({ )}

) : ( -

+

No video playing

)} @@ -481,7 +516,7 @@ export default function StreamView({ diff --git a/next-app/components/StreamView/AddSongForm.tsx b/next-app/components/StreamView/AddSongForm.tsx index c6186b1..3b21f77 100644 --- a/next-app/components/StreamView/AddSongForm.tsx +++ b/next-app/components/StreamView/AddSongForm.tsx @@ -4,12 +4,15 @@ import React from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; -import LiteYouTubeEmbed from "react-lite-youtube-embed"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; -import { LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction } from "@solana/web3.js"; +import { + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, +} from "@solana/web3.js"; import { useSession } from "next-auth/react"; - - +import Image from "next/image"; type Props = { inputLink: string; @@ -19,8 +22,8 @@ type Props = { setInputLink: (value: string) => void; loading: boolean; enqueueToast: (type: "error" | "success", message: string) => void; - spaceId:string, - isSpectator:boolean + spaceId: string; + isSpectator: boolean; }; export default function AddSongForm({ @@ -35,14 +38,14 @@ export default function AddSongForm({ }: Props) { const { sendMessage } = useSocket(); const wallet = useWallet(); - const {connection} = useConnection(); + const { connection } = useConnection(); const user = useSession().data?.user; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (inputLink.match(YT_REGEX)) { setLoading(true); - + sendMessage("add-to-queue", { spaceId, userId, @@ -58,52 +61,50 @@ export default function AddSongForm({ const handlePayAndPlay = async (e: React.FormEvent) => { e.preventDefault(); - if(!wallet.publicKey || !connection){ + if (!wallet.publicKey || !connection) { enqueueToast("error", "Please connect your wallet"); return; } if (!inputLink.match(YT_REGEX)) { enqueueToast("error", "Invalid please use specified formate"); } - try{ + try { setLoading(true); const transaction = new Transaction(); transaction.add( - SystemProgram.transfer({ - fromPubkey: wallet.publicKey, - toPubkey: new PublicKey(process.env.NEXT_PUBLIC_PUBLICKEY as string), - lamports: Number(process.env.NEXT_PUBLIC_SOL_PER_PAYMENT) * LAMPORTS_PER_SOL, - }) - ) + SystemProgram.transfer({ + fromPubkey: wallet.publicKey, + toPubkey: new PublicKey(process.env.NEXT_PUBLIC_PUBLICKEY as string), + lamports: + Number(process.env.NEXT_PUBLIC_SOL_PER_PAYMENT) * LAMPORTS_PER_SOL, + }), + ); - // sign Transaction steps + // sign Transaction steps const blockHash = await connection.getLatestBlockhash(); transaction.feePayer = wallet.publicKey; transaction.recentBlockhash = blockHash.blockhash; //@ts-ignore const signed = await wallet.signTransaction(transaction); - const signature = await connection.sendRawTransaction(signed.serialize()); - + enqueueToast("success", `Transaction signature: ${signature}`); await connection.confirmTransaction({ - blockhash: blockHash.blockhash, - lastValidBlockHeight: blockHash.lastValidBlockHeight, - signature + blockhash: blockHash.blockhash, + lastValidBlockHeight: blockHash.lastValidBlockHeight, + signature, }); enqueueToast("success", `Payment successful`); sendMessage("pay-and-play-next", { spaceId, userId: user?.id, - url:inputLink + url: inputLink, }); - } - catch(error){ + } catch (error) { enqueueToast("error", `Payment unsuccessful`); } setLoading(false); - }; const videoId = inputLink ? inputLink.match(YT_REGEX)?.[1] : undefined; @@ -129,8 +130,8 @@ export default function AddSongForm({ > {loading ? "Loading..." : "Add to Queue"} - - { isSpectator && + + {isSpectator && ( - } - + )} {videoId && !loading && ( - + Thumbnail )} diff --git a/next-app/next.config.mjs b/next-app/next.config.mjs index 76c1ebc..f8f9b80 100644 --- a/next-app/next.config.mjs +++ b/next-app/next.config.mjs @@ -3,7 +3,7 @@ const nextConfig = { reactStrictMode: false, output: 'standalone', images: { - domains: ['images.unsplash.com','i.ytimg.com'], + domains: ['images.unsplash.com','i.ytimg.com','img.youtube.com'], }, }; diff --git a/next-app/package.json b/next-app/package.json index 49086a1..488f5e1 100644 --- a/next-app/package.json +++ b/next-app/package.json @@ -28,7 +28,7 @@ "@solana/wallet-adapter-react-ui": "^0.9.35", "@solana/wallet-adapter-wallets": "^0.19.32", "@solana/web3.js": "^1.95.3", - "axios": "^1.7.5", + "axios": "^1.7.9", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -47,7 +47,6 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "youtube-player": "^5.6.0", - "youtube-search-api": "^1.2.2", "zod": "^3.23.8" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..efdb67a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "muzer", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}