Skip to content

Commit

Permalink
download service migrated to its own api
Browse files Browse the repository at this point in the history
  • Loading branch information
stylessh committed Dec 15, 2022
0 parents commit 23f0d7c
Show file tree
Hide file tree
Showing 8 changed files with 1,354 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules

.env
.env.local
26 changes: 26 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "ytdl-api",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "nodemon ./src/app.ts",
"build": "rm -rf ./dist/ && tsc"
},
"dependencies": {
"axios": "^1.2.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"ffmpeg-static": "^5.1.0",
"morgan": "^1.10.0",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"typescript": "^4.9.4",
"ytdl-core": "^4.11.2"
},
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.15",
"@types/morgan": "^1.9.3"
}
}
45 changes: 45 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import express from "express";
import cors from "cors";
import morgan from "morgan";

import videoRoutes from "./routes/video.routes";

const app = express();

const allowedOrigins = [
"http://localhost:3000",
"https://yt-downloader-jet.vercel.app/",
];

// settings
app.set("port", process.env.PORT || 8000);

// middlewares
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) === -1) {
var msg =
"The CORS policy for this site does not " +
"allow access from the specified Origin.";
return callback(new Error(msg), false);
}
return callback(null, true);
},
})
);
app.use(morgan("dev"));
app.use(express.json());

// routes
app.use("/api/video", videoRoutes);

app.get("/", (req, res) => {
res.status(200).json({ message: "Welcome to the API" });
});

// start
app.listen(app.get("port"), () => {
console.log("Server on port", app.get("port"));
});
97 changes: 97 additions & 0 deletions src/controllers/video.controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Request, Response } from "express";
import ytdl from "ytdl-core";
import downloadVideoWithAudio from "../utils/downloadVideoWithAudio";

import cp from "child_process";
import ffmpeg from "ffmpeg-static";

type BodyData = {
url: string;
quality?: number;
trim?: TrimData;
};

type TrimData = {
start: string;
end: string;
};

export const download = async (req: Request, res: Response) => {
const { url, quality, trim } = req.body as BodyData;

// console.log("trim", trim);

res.setHeader("Content-Disposition", `attachment; filename="video.mp4"`);
res.setHeader("Content-Type", "video/mp4");

// 18 = 360p this always come with audio so we don't need to download audio
if (quality === 18) {
const info = await ytdl.getInfo(url);

// get 360p video
const format = ytdl.chooseFormat(info.formats, { quality: quality });

const video = ytdl(url, { format });

// if trim is set then trim the video
if (trim) {
const ffmpegProcess = cp.spawn(
ffmpeg || "d:\\ffmpeg\\bin\\ffmpeg.exe",
[
// Remove ffmpeg's console spamming
"-loglevel",
"0",
"-hide_banner",
// inputs
"-i",
"pipe:3",
// trim video
"-ss",
trim.start,
"-to",
trim.end,
// Choose some fancy codes
"-c:v",
"copy",
"-c:a",
"copy",
// Define output container
"-f",
"matroska",
"pipe:4",
],
{
windowsHide: true,
stdio: [
/* Standard: stdin, stdout, stderr */
"inherit",
"inherit",
"inherit",
/* Custom: pipe:3, pipe:4 */
"pipe",
"pipe",
],
}
);

ffmpegProcess.on("close", () => {
process.stdout.write("\n");
});

video.pipe(ffmpegProcess.stdio[3] as any);
ffmpegProcess.stdio[4]?.pipe(res);
} else {
// if trim is not set then just download the video
video.pipe(res);
}
} else {
// download video with audio
downloadVideoWithAudio({ url, quality, trim }, res);
}
};

export const config = {
api: {
responseLimit: false,
},
};
7 changes: 7 additions & 0 deletions src/routes/video.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Router } from "express";
const router = Router();
import { download } from "../controllers/video.controllers";

router.post("/download", download);

export default router;
89 changes: 89 additions & 0 deletions src/utils/downloadVideoWithAudio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Response } from "express";
import ytdl from "ytdl-core";
import cp from "child_process";
import ffmpeg from "ffmpeg-static";

type BodyData = {
url: string;
quality?: number;
trim?: TrimData;
};

type TrimData = {
start: string;
end: string;
};

export default async function downloadVideoWithAudio(
data: BodyData,
res: Response
) {
const info = await ytdl.getInfo(data.url);

const videoFormat = ytdl.chooseFormat(info.formats, {
quality: data.quality,
filter: "videoonly",
});

// get highest quality audio
const audioFormat = ytdl.chooseFormat(info.formats, {
quality: "highestaudio",
filter: "audioonly",
});

// download video and audio and merge them into one file using ffmpeg
const video = ytdl(data.url, { format: videoFormat });
const audio = ytdl(data.url, { format: audioFormat });

const ffmpegProcess = cp.spawn(
ffmpeg || "d:\\ffmpeg\\bin\\ffmpeg.exe",
[
// Remove ffmpeg's console spamming
"-loglevel",
"0",
"-hide_banner",
// inputs
"-i",
"pipe:3",
"-i",
"pipe:4",
// if trim is set then trim the video
...(data.trim ? ["-ss", data.trim.start, "-to", data.trim.end] : []),
// Choose some fancy codes
"-c:v",
"copy",
"-c:a",
"aac",
// Define output container
"-f",
"matroska",
"pipe:5",
],
{
windowsHide: true,
stdio: [
/* Standard: stdin, stdout, stderr */
"inherit",
"inherit",
"pipe",
"pipe",
"pipe",
"pipe",
],
}
);

ffmpegProcess.on("close", () => {
process.stdout.write("\n");
});

// return video and audio to client
res.status(200);

audio.pipe(ffmpegProcess.stdio[3] as any);
video.pipe(ffmpegProcess.stdio[4] as any);

// ignore error of index
// @ts-ignore
ffmpegProcess.stdio[5]?.pipe(res);
}
43 changes: 43 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"compilerOptions": {
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */

/* Modules */
"module": "NodeNext", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

/* Emit */
"outDir": "./dist", /* Specify an output folder for all emitted files. */

/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */

/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */

/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"baseUrl": "./src"
}
}
Loading

0 comments on commit 23f0d7c

Please sign in to comment.