commit ecf1d45af8d8a1e2002b5bb8148a584371548ac1 Author: NK Date: Thu Apr 27 14:38:02 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81ea8f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +package-lock.json +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..965a122 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +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). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. + +[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.ts`. + +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. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..a843cbe --- /dev/null +++ b/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..98188c4 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "youtube-downloader", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@types/node": "18.16.1", + "@types/react": "18.2.0", + "@types/react-dom": "18.2.1", + "autoprefixer": "^10.4.14", + "downloadjs": "^1.4.7", + "next": "13.3.1", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-icons": "^4.8.0", + "tailwindcss": "^3.3.2", + "typescript": "5.0.4", + "ytdl-core": "npm:@distube/ytdl-core@^4.11.9" + }, + "devDependencies": { + "@types/downloadjs": "^1.4.3" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 0000000..d5bbcf8 --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,7 @@ +import "tailwindcss/tailwind.css"; + +import type { AppProps } from "next/app"; + +export default function App({ Component, pageProps }: AppProps) { + return ; +} diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx new file mode 100644 index 0000000..b2fff8b --- /dev/null +++ b/src/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + + + +
+ + + + ); +} diff --git a/src/pages/api/download.ts b/src/pages/api/download.ts new file mode 100644 index 0000000..5c266fb --- /dev/null +++ b/src/pages/api/download.ts @@ -0,0 +1,30 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +const ytdl = require("ytdl-core"); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === "POST") { + try { + const url = req.body.url; + const type = req.body.type; + if (!ytdl.validateURL(url)) + return res.status(400).json({ message: "Invalid URL", code: 400 }); + if (type !== "mp3" && type !== "mp4") + return res.status(400).json({ message: "Invalid type", code: 400 }); + res.setHeader( + "Content-Type", + type === "mp3" ? "audio/mpeg" : "video/mp4" + ); + await ytdl(url, { + format: type, + filter: type == "mp4" ? "videoandaudio" : "audioonly", + }).pipe(res); + } catch (e) { + console.log(e); + } + } else { + res.status(405).json({ message: "Method not allowed", code: 405 }); + } +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..e3c0f1a --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,150 @@ +import React, { useState } from "react"; +import Head from "next/head"; +import { FiDownloadCloud } from "react-icons/fi"; +import download from "downloadjs"; + +export default function Home() { + const [url, setUrl] = useState(""); + const [info, setInfo] = useState(""); + + const getTitle = async (videoID: string) => { + const youtubeAPI = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoID}&fields=items(id%2Csnippet)&key=AIzaSyB8Fk-MWT_r8nVgG35gIZoP-DhJYpJ_tZ0`; + let response = await fetch(youtubeAPI); + const res = await response.json(); + const title = res.items[0].snippet.title; + return title; + }; + + const getVideoID = (url: string) => { + if (url.match(/watch/)) { + const videoID = url.split("/")[3].split("?")[1].split("=")[1]; + return videoID; + } else if (url.match(/youtu.be/)) { + const videoID = url != "" && url.split("/")[3]; + return videoID; + } + }; + + const handleMp4 = async () => { + const videoID = getVideoID(url); + setInfo("Processing the video..."); + if (videoID) { + const title = await getTitle(videoID); + try { + fetch(`/api/download`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url, type: "mp4" }), + }) + .then(res => res.blob()) + .then(blob => { + const sizeInBytes = blob.size; + console.log("sizeInBytes: ", sizeInBytes); + if (sizeInBytes <= 0) { + setInfo( + "Unable to download! Maybe File size is too high. Try to download video less than 5MB" + ); + } else { + download(blob, `${title}.mp4`, "video/mp4"); + setInfo("Ready for download!"); + } + }); + } catch (err) { + setInfo( + "Unable to download! Maybe File size is too high. Try to download video less than 5MB" + ); + } + } else { + setInfo("Invalid URL"); + } + }; + + const handleMp3 = async () => { + const videoID = getVideoID(url); + setInfo("Processing the video..."); + if (videoID) { + const title = await getTitle(videoID); + try { + const requestOptions = { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url, type: "mp3" }), + }; + fetch(`/api/download`, requestOptions) + .then(res => res.blob()) + .then(blob => { + const sizeInBytes = blob.size; + console.log("sizeInBytes: ", sizeInBytes); + if (sizeInBytes <= 0) { + setInfo( + "Unable to download! Maybe File size is too high. Try to download video less than 5MB" + ); + } else { + download(blob, `${title}.mp3`, "audio/mpeg"); + setInfo("Ready for download!"); + } + }); + } catch (err) { + console.log("err: ", err); + } + } else { + setInfo("Invalid URL"); + } + }; + + return ( +
+ + Youtube Downlaoder + + + +
+
+

+ {" "} +   Youtube Downlaoder{" "} +

+

+ {" "} + Sample video link : https://youtu.be/videoID{" "} +

+
+
+
+ { + setInfo(""); + setUrl(e.target.value); + }} + placeholder="Paste the valid youtube link" + required + > +
+
+
+ + +
+
+ {info &&

{info}

} +
+
+
+ ); +} diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..76fcadc --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,3 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..e182eeb --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +module.exports = { + purge: ["./src/pages/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], + darkMode: false, // or 'media' or 'class' + theme: { + extend: {}, + }, + variants: { + extend: {}, + }, + plugins: [], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..611254a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "postcss.config.js", + "tailwind.config.js" + ], + "exclude": ["node_modules"] +}