youtube-downloader

Simple Next.js website and interface to download YouTube videos to mp3 or mp4 files with URLs
git clone https://codeberg.org/night0721/youtube-downloader
Log | Files | Refs | README | LICENSE

commit ecf1d45af8d8a1e2002b5bb8148a584371548ac1
Author: NK <[email protected]>
Date:   Thu, 27 Apr 2023 14:38:02 +0100

Initial commit

Diffstat:
A.gitignore | 37+++++++++++++++++++++++++++++++++++++
AREADME.md | 38++++++++++++++++++++++++++++++++++++++
Anext.config.js | 6++++++
Apackage.json | 28++++++++++++++++++++++++++++
Apostcss.config.js | 6++++++
Asrc/pages/_app.tsx | 7+++++++
Asrc/pages/_document.tsx | 13+++++++++++++
Asrc/pages/api/download.ts | 30++++++++++++++++++++++++++++++
Asrc/pages/index.tsx | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/styles/globals.css | 3+++
Atailwind.config.js | 11+++++++++++
Atsconfig.json | 29+++++++++++++++++++++++++++++
12 files changed, 358 insertions(+), 0 deletions(-)

diff --git a/.gitignore 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 @@ -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 @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/package.json 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 @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/pages/_app.tsx 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 <Component {...pageProps} />; +} diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + <Html lang="en"> + <Head /> + <body> + <Main /> + <NextScript /> + </body> + </Html> + ); +} diff --git a/src/pages/api/download.ts 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 @@ -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 ( + <div> + <Head> + <title>Youtube Downlaoder</title> + <link rel="icon" href="/icon.png" /> + </Head> + + <div className="py-12 w-full h-screen"> + <div className="max-w-7xl mx-auto sm:px-6 lg:px-8 w-full relative top-1/4"> + <h3 className="text-4xl flex justify-center"> + {" "} + <FiDownloadCloud /> &nbsp; Youtube Downlaoder{" "} + </h3> + <h4 className="text-xs py-1 flex justify-center"> + {" "} + Sample video link : https://youtu.be/videoID{" "} + </h4> + <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg w-full"> + <div className="p-6 bg-white border-b border-gray-200 flex justify-center w-full"> + <div className="flex w-4/5"> + <input + type="text" + className="border-2 m-1.5 border-gray-300 p-2 w-full" + name="title" + id="title" + value={url} + onChange={e => { + setInfo(""); + setUrl(e.target.value); + }} + placeholder="Paste the valid youtube link" + required + ></input> + </div> + </div> + <div className="p-3 flex w-full justify-center"> + <button + className="p-3 m-1.5 flex w-56 justify-center bg-blue-900 text-white hover:bg-blue-600" + onClick={() => handleMp3()} + > + Download mp3 + </button> + <button + className="p-3 m-1.5 flex w-48 justify-center bg-blue-900 text-white hover:bg-blue-600" + onClick={() => handleMp4()} + > + Download mp4 + </button> + </div> + </div> + {info && <h3 className="flex justify-center p-3 m-1.5 "> {info} </h3>} + </div> + </div> + </div> + ); +} diff --git a/src/styles/globals.css 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 @@ -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 @@ -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"] +}