├── .eslintrc.json ├── public ├── 1.webp ├── 2.webp ├── 3.webp ├── og.png ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── site.webmanifest ├── jsconfig.json ├── next.config.mjs ├── src ├── pages │ ├── _app.js │ ├── _document.js │ ├── api │ │ └── index.js │ └── index.js ├── lib │ └── utils.js ├── components │ ├── LoadingState.jsx │ ├── crisp.jsx │ ├── ui │ │ ├── input.jsx │ │ ├── card.jsx │ │ ├── button.jsx │ │ ├── table.jsx │ │ ├── dialog.jsx │ │ └── command.jsx │ ├── Navbar.jsx │ ├── Search.jsx │ ├── Footer.jsx │ ├── Landing.jsx │ └── Result.jsx ├── utils │ ├── createWordFrequencyMap.js │ ├── microUtils.js │ └── decodeHtmlEntities.js └── styles │ └── globals.css ├── postcss.config.mjs ├── components.json ├── .gitignore ├── package.json ├── tailwind.config.js ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/1.webp -------------------------------------------------------------------------------- /public/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/2.webp -------------------------------------------------------------------------------- /public/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/3.webp -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/og.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VishwaGauravIn/youtube-word-frequency-counter/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | export default nextConfig; 7 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | 3 | export default function App({ Component, pageProps }) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/components/LoadingState.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function LoadingState() { 4 | return
5 |
6 |
; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": false, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/utils/createWordFrequencyMap.js: -------------------------------------------------------------------------------- 1 | export default function createWordFrequencyMap(text) { 2 | const wordRegex = /[^\s-]+/g; // Updated regex to include non-space characters and hyphens 3 | const words = text.match(wordRegex); 4 | const wordFrequency = {}; 5 | 6 | if (!words) return []; 7 | 8 | for (const word of words) { 9 | const normalizedWord = word.toLowerCase(); 10 | wordFrequency[normalizedWord] = (wordFrequency[normalizedWord] || 0) + 1; 11 | } 12 | return Object.entries(wordFrequency) 13 | .map(([word, frequency]) => ({ word, frequency })) 14 | .sort((a, b) => b.frequency - a.frequency); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/crisp.jsx: -------------------------------------------------------------------------------- 1 | { 2 | /* Disabled due to spam */ 3 | } 4 | 5 | import dynamic from "next/dynamic"; 6 | 7 | import { Crisp } from "crisp-sdk-web"; 8 | import { Component } from "react"; 9 | 10 | const CrispWithNoSSR = dynamic(() => import("./crisp"), { 11 | ssr: false, 12 | }); 13 | 14 | class CrispChat extends Component { 15 | componentDidMount() { 16 | Crisp.configure("a5e53b55-a08e-4ebe-bd54-14aff455962b"); 17 | } 18 | 19 | render() { 20 | return null; 21 | } 22 | } 23 | export default CrispChat; 24 | 25 | // open crisp chat 26 | export const openCrispChat = () => { 27 | Crisp.chat.open(); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/ui/input.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef(({ className, type, ...props }, ref) => { 6 | return ( 7 | () 15 | ); 16 | }) 17 | Input.displayName = "Input" 18 | 19 | export { Input } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yt-word-count", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.0.5", 13 | "@radix-ui/react-slot": "^1.0.2", 14 | "axios": "^1.7.2", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "cmdk": "^1.0.0", 18 | "crisp-sdk-web": "^1.0.25", 19 | "js-num-prettier": "^0.1.0", 20 | "lucide-react": "^0.394.0", 21 | "next": "14.2.4", 22 | "react": "^18", 23 | "react-dom": "^18", 24 | "sonner": "^1.5.0", 25 | "tailwind-merge": "^2.3.0", 26 | "tailwindcss-animate": "^1.0.7", 27 | "ytdl-core": "^4.11.5" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^8", 31 | "eslint-config-next": "14.2.4", 32 | "postcss": "^8", 33 | "tailwindcss": "^3.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/api/index.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import { getCaptionUrl } from "@/utils/microUtils"; 3 | import ytdl from "ytdl-core"; 4 | 5 | export default async function handler(req, res) { 6 | const videoId = req.query.id; 7 | let info = await ytdl.getInfo(videoId); 8 | let captionArray = 9 | info.player_response.captions.playerCaptionsTracklistRenderer.captionTracks; 10 | let videoDetails = info.player_response.videoDetails; 11 | 12 | // Set cache headers for 1 day (86400 seconds) 13 | res.setHeader("Cache-Control", "public, max-age=86400"); 14 | 15 | res.json({ 16 | videoDetails: { 17 | videoId: videoDetails.videoId, 18 | title: videoDetails.title, 19 | viewCount: videoDetails.viewCount, 20 | author: videoDetails.author, 21 | thumbnail: { 22 | thumbnails: [videoDetails.thumbnail.thumbnails[0]], 23 | }, 24 | }, 25 | captionUrl: getCaptionUrl(captionArray), 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { Github, Youtube } from "lucide-react"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import { Button } from "./ui/button"; 5 | 6 | export default function Navbar({ resetState }) { 7 | return ( 8 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Input } from "./ui/input"; 3 | import { SearchIcon } from "lucide-react"; 4 | 5 | export default function Search({ url, setUrl, handleSubmit, loading }) { 6 | return ( 7 |
8 |
12 | 13 | { 20 | !loading && setUrl(e.target.value); 21 | }} 22 | disabled={loading} 23 | /> 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/microUtils.js: -------------------------------------------------------------------------------- 1 | export function getTextFromXML(xml) { 2 | const parser = new DOMParser(); 3 | const xmlDoc = parser.parseFromString(xml, "text/xml"); 4 | const textElements = xmlDoc.getElementsByTagName("text"); 5 | const textContents = Array.from(textElements) 6 | .map((el) => el.textContent) 7 | .join(" "); 8 | return textContents; 9 | } 10 | 11 | export function getCaptionUrl(captionArray) { 12 | let englishCaption = null; 13 | let nonAutoGeneratedCaption = null; 14 | let autoGeneratedCaption = null; 15 | 16 | for (const caption of captionArray) { 17 | if (caption.languageCode === "en") { 18 | englishCaption = caption.baseUrl; 19 | break; 20 | } else if ( 21 | !caption.trackName.includes("(auto-generated)") && 22 | !nonAutoGeneratedCaption 23 | ) { 24 | nonAutoGeneratedCaption = caption.baseUrl; 25 | } else if ( 26 | caption.trackName.includes("(auto-generated)") && 27 | !autoGeneratedCaption 28 | ) { 29 | autoGeneratedCaption = caption.baseUrl; 30 | } 31 | } 32 | 33 | return ( 34 | englishCaption || 35 | nonAutoGeneratedCaption || 36 | autoGeneratedCaption || 37 | captionArray[0].baseUrl 38 | ); 39 | } 40 | 41 | export function getAlphaNumeric(string) { 42 | // alphanumeric with - and ' and space 43 | return string.replace(/[\p{P}\p{S}]/gu, ""); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ui/card.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef(({ className, ...props }, ref) => ( 6 |
10 | )) 11 | Card.displayName = "Card" 12 | 13 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( 14 |
18 | )) 19 | CardHeader.displayName = "CardHeader" 20 | 21 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( 22 |

26 | )) 27 | CardTitle.displayName = "CardTitle" 28 | 29 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( 30 |

34 | )) 35 | CardDescription.displayName = "CardDescription" 36 | 37 | const CardContent = React.forwardRef(({ className, ...props }, ref) => ( 38 |

39 | )) 40 | CardContent.displayName = "CardContent" 41 | 42 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( 43 |
47 | )) 48 | CardFooter.displayName = "CardFooter" 49 | 50 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 51 | -------------------------------------------------------------------------------- /src/components/ui/button.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { 37 | const Comp = asChild ? Slot : "button" 38 | return ( 39 | () 43 | ); 44 | }) 45 | Button.displayName = "Button" 46 | 47 | export { Button, buttonVariants } 48 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Github, Instagram, Linkedin, Twitter, X } from "lucide-react"; 2 | import React from "react"; 3 | 4 | export default function Footer() { 5 | return ( 6 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{js,jsx}', 6 | './components/**/*.{js,jsx}', 7 | './app/**/*.{js,jsx}', 8 | './src/**/*.{js,jsx}', 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | }, 70 | animation: { 71 | "accordion-down": "accordion-down 0.2s ease-out", 72 | "accordion-up": "accordion-up 0.2s ease-out", 73 | }, 74 | }, 75 | }, 76 | plugins: [require("tailwindcss-animate")], 77 | } -------------------------------------------------------------------------------- /src/components/ui/table.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef(({ className, ...props }, ref) => ( 6 |
7 | 11 | 12 | )) 13 | Table.displayName = "Table" 14 | 15 | const TableHeader = React.forwardRef(({ className, ...props }, ref) => ( 16 | 17 | )) 18 | TableHeader.displayName = "TableHeader" 19 | 20 | const TableBody = React.forwardRef(({ className, ...props }, ref) => ( 21 | 25 | )) 26 | TableBody.displayName = "TableBody" 27 | 28 | const TableFooter = React.forwardRef(({ className, ...props }, ref) => ( 29 | tr]:last:border-b-0", className)} 32 | {...props} /> 33 | )) 34 | TableFooter.displayName = "TableFooter" 35 | 36 | const TableRow = React.forwardRef(({ className, ...props }, ref) => ( 37 | 44 | )) 45 | TableRow.displayName = "TableRow" 46 | 47 | const TableHead = React.forwardRef(({ className, ...props }, ref) => ( 48 |
55 | )) 56 | TableHead.displayName = "TableHead" 57 | 58 | const TableCell = React.forwardRef(({ className, ...props }, ref) => ( 59 | 63 | )) 64 | TableCell.displayName = "TableCell" 65 | 66 | const TableCaption = React.forwardRef(({ className, ...props }, ref) => ( 67 |
71 | )) 72 | TableCaption.displayName = "TableCaption" 73 | 74 | export { 75 | Table, 76 | TableHeader, 77 | TableBody, 78 | TableFooter, 79 | TableHead, 80 | TableRow, 81 | TableCell, 82 | TableCaption, 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |


YouTube Word Counter

3 | 4 | 5 | 6 | 7 |

8 | Discover What YouTubers Say the Most 9 |
10 |
11 | 12 | # Demo 13 | 14 | https://github.com/VishwaGauravIn/youtube-word-frequency-counter/assets/81325730/cf1063ff-16a6-48c5-acda-37ec6272fb01 15 | 16 | Discover top words & catchphrases in YouTube videos! Analyze word frequency & understand creators' content deeper. Free YouTube word counter. [Try now!](https://ytword.itsvg.in/) 17 | 18 | # Features 19 | - 🔠 Supports All languages 20 | - ✅ Supports 99.99% Videos 21 | - 💲 100% Free to use 22 | 23 | ## [🚀 Try now!](https://ytword.itsvg.in/) 24 | 25 | ## Tops in all departments 26 |
27 | 28 | ![psi](https://github.com/VishwaGauravIn/youtube-word-frequency-counter/assets/81325730/eb1e6103-9516-4909-bf79-c1a4af930bfb) 29 | 30 | 31 | 32 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 33 | 34 | ## Getting Started 35 | 36 | First, run the development server: 37 | 38 | ```bash 39 | npm run dev 40 | # or 41 | yarn dev 42 | # or 43 | pnpm dev 44 | # or 45 | bun dev 46 | ``` 47 | 48 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 49 | 50 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 51 | 52 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 53 | 54 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 55 | 56 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 57 | 58 |
59 | 60 | Learn More 61 | 62 | 63 | ## Learn More 64 | 65 | To learn more about Next.js, take a look at the following resources: 66 | 67 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 68 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 69 | 70 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 71 | 72 |
73 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | .loader { 79 | font-weight:300; 80 | font-family: monospace; 81 | display: inline-grid; 82 | font-size: 30px; 83 | opacity: 0.5; 84 | } 85 | .loader:before, 86 | .loader:after { 87 | content: "Loading..."; 88 | grid-area: 1/1; 89 | -webkit-mask-size: 2ch 100%, 100% 100%; 90 | -webkit-mask-repeat: no-repeat; 91 | -webkit-mask-composite: xor; 92 | mask-composite: exclude; 93 | animation: l37 1s infinite; 94 | } 95 | .loader:before { 96 | -webkit-mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0); 97 | } 98 | .loader:after { 99 | -webkit-mask-image: linear-gradient(#000 0 0); 100 | transform: scaleY(0.5); 101 | } 102 | 103 | @keyframes l37 { 104 | 0% { 105 | -webkit-mask-position: 1ch 0, 0 0; 106 | } 107 | 12.5% { 108 | -webkit-mask-position: 100% 0, 0 0; 109 | } 110 | 25% { 111 | -webkit-mask-position: 4ch 0, 0 0; 112 | } 113 | 37.5% { 114 | -webkit-mask-position: 8ch 0, 0 0; 115 | } 116 | 50% { 117 | -webkit-mask-position: 2ch 0, 0 0; 118 | } 119 | 62.5% { 120 | -webkit-mask-position: 100% 0, 0 0; 121 | } 122 | 75% { 123 | -webkit-mask-position: 0ch 0, 0 0; 124 | } 125 | 87.5% { 126 | -webkit-mask-position: 6ch 0, 0 0; 127 | } 128 | 100% { 129 | -webkit-mask-position: 3ch 0, 0 0; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/components/Landing.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card } from "./ui/card"; 3 | import { Check, Clock, DollarSign, Languages } from "lucide-react"; 4 | 5 | export default function Landing({ setURL, getResult }) { 6 | function handleClick(id) { 7 | setURL("https://www.youtube.com/watch?v=" + id); 8 | getResult("https://www.youtube.com/watch?v=" + id); 9 | } 10 | 11 | function videoCard({ title, duration, id, imgID }) { 12 | return ( 13 | { 16 | handleClick(id); 17 | }} 18 | > 19 | 24 |
25 |
26 |

27 | {title} 28 | 29 | 30 | {duration} 31 | 32 |

33 |
34 | 35 | ); 36 | } 37 | return ( 38 |
39 |
40 |

41 | Discover What YouTubers Say the Most 42 |

43 |

44 | Discover the Most Frequent Words in YouTube Videos, Word Frequency 45 | Analyzer for YouTube. 46 |

47 |
48 |
49 |
50 | 51 | Supports All languages 52 |
53 |
54 | 55 | Supports 99.99% Videos 56 |
57 |
58 | 59 | 100% Free to use 60 |
61 |
62 |

63 | Try with these Videos 64 |

65 |
66 | {/* 1 */} 67 | {videoCard({ 68 | title: "Joe Rogan & Elon Musk - Are We in a Simulated Reality?", 69 | duration: "15:02", 70 | id: "0cM690CKArQ", 71 | imgID: "1", 72 | })} 73 | {/* 2 */} 74 | {videoCard({ 75 | title: "MrBeast Gets Flagrant and Walked Away from $1 BILLION", 76 | duration: "2:44:30", 77 | id: "WGrk7Mzm4uo", 78 | imgID: "2", 79 | })} 80 | {/* 3 */} 81 | {videoCard({ 82 | title: "The Hunt for the King of the Dark Web", 83 | duration: "18:50", 84 | id: "gq01C2kjMiM", 85 | imgID: "3", 86 | })} 87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/ui/dialog.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { X } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( 16 | 23 | )) 24 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 25 | 26 | const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => ( 27 | 28 | 29 | 36 | {children} 37 | 39 | 40 | Close 41 | 42 | 43 | 44 | )) 45 | DialogContent.displayName = DialogPrimitive.Content.displayName 46 | 47 | const DialogHeader = ({ 48 | className, 49 | ...props 50 | }) => ( 51 |
54 | ) 55 | DialogHeader.displayName = "DialogHeader" 56 | 57 | const DialogFooter = ({ 58 | className, 59 | ...props 60 | }) => ( 61 |
64 | ) 65 | DialogFooter.displayName = "DialogFooter" 66 | 67 | const DialogTitle = React.forwardRef(({ className, ...props }, ref) => ( 68 | 72 | )) 73 | DialogTitle.displayName = DialogPrimitive.Title.displayName 74 | 75 | const DialogDescription = React.forwardRef(({ className, ...props }, ref) => ( 76 | 80 | )) 81 | DialogDescription.displayName = DialogPrimitive.Description.displayName 82 | 83 | export { 84 | Dialog, 85 | DialogPortal, 86 | DialogOverlay, 87 | DialogClose, 88 | DialogTrigger, 89 | DialogContent, 90 | DialogHeader, 91 | DialogFooter, 92 | DialogTitle, 93 | DialogDescription, 94 | } 95 | -------------------------------------------------------------------------------- /src/components/Result.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Eye, Forward } from "lucide-react"; 3 | import { Card } from "./ui/card"; 4 | import numFormatter from "js-num-prettier"; 5 | import { 6 | Command, 7 | CommandEmpty, 8 | CommandInput, 9 | CommandItem, 10 | CommandList, 11 | } from "./ui/command"; 12 | import { 13 | Table, 14 | TableBody, 15 | TableCaption, 16 | TableCell, 17 | TableHead, 18 | TableHeader, 19 | TableRow, 20 | } from "@/components/ui/table"; 21 | import { Button } from "./ui/button"; 22 | 23 | export default function Result({ info }) { 24 | function handleShare() { 25 | const shareData = { 26 | title: "YouTube Word Frequency Analyzer", 27 | text: 28 | "Check out the most repeated words in this YouTube video: " + 29 | info.videoDetails.title, 30 | url: 31 | "https://ytword.itsvg.in/?url=https://www.youtube.com/watch?v=" + 32 | info.videoDetails.videoId, 33 | }; 34 | 35 | if (navigator.share) { 36 | navigator 37 | .share(shareData) 38 | .catch((error) => toast.error("Error sharing", error)); 39 | } else { 40 | toast.erro( 41 | "Web Share API is not supported in your browser, you can copy the link to share." 42 | ); 43 | } 44 | } 45 | return ( 46 |
47 | 48 | 53 |
54 |

{info.videoDetails.title}

55 |

{info.videoDetails.author}

56 |

57 | {" "} 58 | {numFormatter(info.videoDetails.viewCount, 2)} views 59 |

60 |
61 |
62 | 63 | 64 | 65 | No results found. 66 | {info.wordFrequency.map((word, i) => ( 67 | 68 |

69 | {word.word} 70 | 71 | ({word.frequency} times) 72 | 73 |

74 |
75 | ))} 76 |
77 |
78 |

79 | Words with highest occurence: 80 |

81 | 82 | 83 | Top 10 words with highest occurence 84 | 85 | 86 | 87 | Word 88 | Occurence 89 | 90 | 91 | 92 | {info.wordFrequency.slice(0, 10).map((word, i) => ( 93 | 94 | {i + 1}. 95 | {word.word} 96 | 97 | {word.frequency} times 98 | 99 | 100 | ))} 101 | 102 |
103 |
104 | 107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /src/components/ui/command.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Command as CommandPrimitive } from "cmdk" 3 | import { Search } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | import { Dialog, DialogContent } from "@/components/ui/dialog" 7 | 8 | const Command = React.forwardRef(({ className, ...props }, ref) => ( 9 | 16 | )) 17 | Command.displayName = CommandPrimitive.displayName 18 | 19 | const CommandDialog = ({ 20 | children, 21 | ...props 22 | }) => { 23 | return ( 24 | ( 25 | 26 | 28 | {children} 29 | 30 | 31 | ) 32 | ); 33 | } 34 | 35 | const CommandInput = React.forwardRef(({ className, ...props }, ref) => ( 36 |
37 | 38 | 45 |
46 | )) 47 | 48 | CommandInput.displayName = CommandPrimitive.Input.displayName 49 | 50 | const CommandList = React.forwardRef(({ className, ...props }, ref) => ( 51 | 55 | )) 56 | 57 | CommandList.displayName = CommandPrimitive.List.displayName 58 | 59 | const CommandEmpty = React.forwardRef((props, ref) => ( 60 | 61 | )) 62 | 63 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 64 | 65 | const CommandGroup = React.forwardRef(({ className, ...props }, ref) => ( 66 | 73 | )) 74 | 75 | CommandGroup.displayName = CommandPrimitive.Group.displayName 76 | 77 | const CommandSeparator = React.forwardRef(({ className, ...props }, ref) => ( 78 | 79 | )) 80 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 81 | 82 | const CommandItem = React.forwardRef(({ className, ...props }, ref) => ( 83 | 90 | )) 91 | 92 | CommandItem.displayName = CommandPrimitive.Item.displayName 93 | 94 | const CommandShortcut = ({ 95 | className, 96 | ...props 97 | }) => { 98 | return ( 99 | () 102 | ); 103 | } 104 | CommandShortcut.displayName = "CommandShortcut" 105 | 106 | export { 107 | Command, 108 | CommandDialog, 109 | CommandInput, 110 | CommandList, 111 | CommandEmpty, 112 | CommandGroup, 113 | CommandItem, 114 | CommandShortcut, 115 | CommandSeparator, 116 | } 117 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | itsvgin@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import CrispChat from "@/components/crisp"; 2 | import Footer from "@/components/Footer"; 3 | import Landing from "@/components/Landing"; 4 | import LoadingState from "@/components/LoadingState"; 5 | import Navbar from "@/components/Navbar"; 6 | import Result from "@/components/Result"; 7 | import Search from "@/components/Search"; 8 | import createWordFrequencyMap from "@/utils/createWordFrequencyMap"; 9 | import decodeHtmlEntities from "@/utils/decodeHtmlEntities"; 10 | import { getAlphaNumeric, getTextFromXML } from "@/utils/microUtils"; 11 | import axios from "axios"; 12 | import { Inter } from "next/font/google"; 13 | import Head from "next/head"; 14 | import { useRouter } from "next/router"; 15 | import Script from "next/script"; 16 | import { useState } from "react"; 17 | import { Toaster, toast } from "sonner"; 18 | import ytdl from "ytdl-core"; 19 | 20 | const inter = Inter({ subsets: ["latin"] }); 21 | 22 | export default function Home({ query }) { 23 | const [url, setUrl] = useState(""); 24 | const [info, setInfo] = useState(null); 25 | const [loading, setLoading] = useState(false); 26 | const { isReady, replace } = useRouter(); 27 | 28 | async function handleSubmit(e) { 29 | e?.preventDefault(); 30 | getResult(url); 31 | } 32 | 33 | async function getResult(videoURL) { 34 | if (ytdl.validateURL(videoURL)) { 35 | setLoading(true); 36 | setInfo(null); 37 | const videoID = ytdl.getVideoID(videoURL); 38 | let info; 39 | await axios 40 | .get(`/api?id=${videoID}`) 41 | .then(async (res) => { 42 | const XMLCaptions = (await axios.get(res.data.captionUrl)).data; 43 | info = { 44 | videoDetails: res.data.videoDetails, 45 | wordFrequency: createWordFrequencyMap( 46 | decodeHtmlEntities(getAlphaNumeric(getTextFromXML(XMLCaptions))) 47 | ), 48 | }; 49 | }) 50 | .catch((err) => { 51 | console.error(err); 52 | toast.error("An error occurred while fetching the video information"); 53 | }) 54 | .finally(() => setLoading(false)); 55 | setInfo(info); 56 | if (typeof window !== "undefined") { 57 | if (isReady) { 58 | replace("/?url=" + videoURL); 59 | } 60 | } 61 | } else { 62 | toast.error("Invalid YouTube video URL"); 63 | } 64 | } 65 | 66 | useState(() => { 67 | if (query?.url) { 68 | if (ytdl.validateURL(query.url)) { 69 | setUrl(query.url); 70 | getResult(query.url); 71 | } else { 72 | setTimeout(() => { 73 | if (typeof window !== "undefined") 74 | if (isReady) { 75 | replace("/"); 76 | toast.error("Invalid YouTube video link in URL"); 77 | } 78 | }, 1000); 79 | } 80 | } 81 | }, []); 82 | 83 | return ( 84 | <> 85 | 86 | 87 | YouTube Word Counter: Discover What YouTubers Say the Most 88 | 89 | 93 | 97 | 98 | 102 | 103 | 104 | 108 | 112 | 113 | 114 | 115 | 119 | 123 | 127 | 128 |