Coding After Thirty

How To Build A Simple CLI Tool Node JS

How To Build A Simple CLI

A CLI, or Command Line Interface, is a type of user interface that allows users to interact with a program by entering commands in the form of text lines. It operates in a text-based environment, usually a terminal or console, where users input commands and receive text responses from the program.

What Are We Going To Build

We are going to build a simple CLI tool that will allow you to download videos from youtube as well as convert them to audio files.

Prerequesetes

Make sure that you have node and npm installed. We will use it to download necessary packages that we need for you CLI.

	node -v
	npm -v

Getting Started

Create directory for you CLI tool:

	mkdir cli-tool
	cd cli-tool

Init yout project by running npm init

➜  cli npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (cli) y
version: (1.0.0)
description: CLI tool do download YT videos and convert to MP3
entry point: (index.js)
test command:
git repository:
keywords:
author: Paul Bratslavsky
license: (ISC)
About to write to /Users/paulbratslavsky/Desktop/stream/cli/package.json:

{
  "name": "cli-tool",
  "version": "1.0.0",
  "description": "CLI tool do download YT videos and convert to MP3",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Paul Bratslavsky",
  "license": "ISC"
}

Open your project in vs code

Once your project is open go ahead and create an entry file called index.js

What Is Cammander

commander is a popular Node.js package that helps to build command-line interfaces (CLIs) in an easy and organized manner. It simplifies the process of parsing command-line arguments, defining commands, handling flags, options, and displaying help documentation.

Let's go ahead and install it now by running the following command:

npm install commander

Once commander is installed let's start building out our CLI.

Make Your Script Executable

Add the following at the top of your index.js file:

#!/usr/bin/env node

This command tells our operating system to use node to execute our command.

Now update your package.json file to include the following code:

"bin": { "cli-tool": "./index.js" },

The above command will alow us to run our cli by typing cli-tool in our terminal.

But before we can get it to work. We need to install it globally by creating a global symlink. We can do it by running the following command:

npm link

Now before testing it out. Let's add a simple console.log in our index.js file

#!/usr/bin/env node
console.log("Hello CLI World");

note: if you need to unlink your CLI tool you can run the following command:

npm unlink <name-of-package>

example:

npm unlink cli-tool

we can test our CLI tool by running the following:

cli-tool

Commander Template Example

Let's set up commander example so we can use it as reference as we build out our cli command tool.

Let's enter the following code below.

#!/usr/bin/env node
console.log("Hello CLI World");

const { program } = require("commander");

// Define the CLI tool version
program.version("1.0.0");

// Define a command with options and a description
program
  .command("greet <name>")
  .option("-c, --capitalize", "Capitalize the name")
  .description("Greets the user with their name")
  .action((name, options) => {
    let output = `Hello, ${options.capitalize ? name.toUpperCase() : name}!`;
    console.log(output);
  });

// Parse command line arguments
program.parse(process.argv);

The cool part about commander is that it automatically creates help documentation.

You can run the help command with the following code:

cli-tool help

Output:

➜  cli cli-tool help
Hello CLI World
Usage: cli-tool [options] [command]

Options:
  -V, --version           output the version number
  -h, --help              display help for command

Commands:
  greet [options] <name>  Greets the user with their name
  help [command]          display help for command
➜  cli

Let's test out our commander tool, type in the following:

cli-tool greet -c paul

Output:

Hello CLI World
Hello, PAUL!

Now that we have the basic down. Lets create a file that will have our logic to download a video from youtube.

Video Download Logic

Inside your folder create a file called video-download.js

Paste in the following code: vide-download.js

// Import the 'fs' module to interact with the file system
const fs = require("fs");

// Import the 'path' module to handle file and directory paths
const path = require("path");

// Import the 'ytdl-core' module to download YouTube videos
const ytdl = require("ytdl-core");

// Define an asynchronous function called 'downloadVideo' that takes three arguments:

// 'videoUrl', 'folderName', and 'fileName'

async function downloadVideo(videoUrl, folderName, fileName) {
  // Create the output file path by joining the folder name and file name (with .mp4 extension)

  const outputFile = path.join(folderName, `${fileName}.mp4`);

  // Check if the output file already exists; if so, log a message and return the file path

  if (fs.existsSync(outputFile)) {
    console.log("Video already downloaded");
    return outputFile;
  }

  // Check if the specified folder exists; if not, create it

  if (!fs.existsSync(folderName)) {
    fs.mkdirSync(folderName);
  }

  // Create a readable stream for the video using the 'ytdl' module with the highest quality available
  const videoStream = ytdl(videoUrl, { quality: "highest" });

  // Create a writable stream for the output file
  const writeStream = fs.createWriteStream(outputFile);

  // Return a new Promise that resolves with the output file path when the video is downloaded,

  // or rejects with an error if there's an issue during the download

  return new Promise((resolve, reject) => {
    // Pipe the video stream into the write stream, effectively downloading the video
    videoStream.pipe(writeStream);

    // When the write stream finishes, call the 'resolveFunction'
    writeStream.on("finish", () => {
      console.log("Video downloaded");
      resolve(outputFile);
    });

    // If there's an error with the video stream, call the 'rejectFunction' with the error

    videoStream.on("error", () => {
      console.log("Error downloading video");
      reject(err);
    });
  });
}

// Export the 'downloadVideo' function as a module
module.exports = {
  downloadVideo,
};

In order for this to work, we first need to install the package tha will allow us to download videos.

We will be using ytdl-core you can learn more about it here

And you can learn more about node commands here

You can install the package by runnig the following command:

npm install ytdl-core

Commander Download Logic

Add the following code to you index.js file:

const { downloadVideo } = require("./video-download");

// Download command with its options and action

program
  .command("download <videoUrl>")
  .description("Download a YouTube video")
  .option("-f, --folder <folderName>", "Output folder name", "downloads")
  .option(
    "-n, --name <fileName>",
    "Output file name (without extension)",
    "video"
  )
  .action(async (videoUrl, options) => {
    try {
      const { folder: folderName, name: fileName } = options;
      const outputFile = await downloadVideo(videoUrl, folderName, fileName);
      console.log(`Video saved at: ${outputFile}`);
    } catch (err) {
      console.error("An error occurred:", err.message);
    }
  });

Let's test out our new CLI command with the following:

cli-tool download https://www.youtube.com/watch?v=w5MpbkNEM1Q -f videos -n test

Output:

➜  cli cli-tool download https://www.youtube.com/watch\?v\=w5MpbkNEM1Q -f videos -n test
Hello CLI World
Video downloaded
Video saved at: videos/test.mp4

Nice. We were able to download the video. Let's now do our last function that will conver our video to audio.

Convert Video To Audio Logic

In order to conver our video to audio we will need to add additional node packages, but first let's look at the code.

Let's create a new file named convert-video.js

// Import the 'path' module to handle file and directory paths
const path = require("path");
// Import the 'fs' module to interact with the file system
const fs = require("fs");
// Import the 'ffmpeg-static' module to get the path to a statically linked FFmpeg binary
const ffmpegPath = require("ffmpeg-static");
// Import the 'fluent-ffmpeg' module to interact with the FFmpeg library
const ffmpeg = require("fluent-ffmpeg");

// Set the path to the FFmpeg binary for 'fluent-ffmpeg' to use
ffmpeg.setFfmpegPath(ffmpegPath);

// Define an asynchronous function called 'convertVideo' that takes three arguments:
// 'videoFilePath', 'folderName', and 'fileName'
async function convertVideo(videoFilePath, folderName, fileName) {
  // Create the output file path by joining the folder name and file name (with .mp3 extension)
  const outputFile = path.join(folderName, `${fileName}.mp3`);

  // Check if the output file already exists; if so, log a message and return the file path
  if (fs.existsSync(outputFile)) {
    console.log("Audio file already converted");
    return outputFile;
  }

  // Check if the specified folder exists; if not, create it
  if (!fs.existsSync(folderName)) {
    fs.mkdirSync(folderName);
  }

  // Return a new Promise that resolves with the output file path when the conversion is complete,
  // or rejects with an error if there's an issue during the conversion
  return new Promise((resolve, reject) => {
    // Create a new 'fluent-ffmpeg' instance for the input video file
    ffmpeg(videoFilePath)
      // Set the output options to convert the video to MP3 format with the desired settings
      .outputOptions([
        "-vn", // No video (audio only)
        "-acodec",
        "libmp3lame", // Use the LAME MP3 codec
        "-ac",
        "2", // 2 audio channels (stereo)
        "-ab",
        "160k", // 160 kbps audio bitrate
        "-ar",
        "48000", // 48,000 Hz audio sample rate
      ])
      // Save the output file to the specified path
      .save(outputFile)
      // Listen for the 'error' event and reject the Promise with the error if it occurs
      .on("error", (error) => {
        console.error("FFmpeg error:", error);
        reject(error);
      })
      // Listen for the 'end' event and resolve the Promise with the output file path when the conversion is complete
      .on("end", () => {
        console.log("FFmpeg process completed");
        resolve(outputFile);
      });
  });
}

// Export the 'convertVideo' function as a module
module.exports = {
  convertVideo,
};

For this code to work we need to install two new packages ffmpeg-static and fluent-ffmpeg:

You can install both by running the follwing command:

npm install ffmpeg-static fluent-ffmpeg

Great now it's time to hook up this logic using commander.

Convert Video To Audio Commander Logic

Here is the code to hook up our video to audio conver logic using commander.

index.js

const { convertVideo } = require("./convert-video");

// Set up video to audio command with its options and action
program
  .command("convert <videoFilePath>")
  .description("Convert a video file to an MP3 audio file")
  .option("-f, --folder <folderName>", "Output folder name", "converted")
  .option(
    "-n, --name <fileName>",
    "Output file name (without extension)",
    "audio"
  )
  .action(async (videoFilePath, options) => {
    try {
      const { folder: folderName, name: fileName } = options;
      const outputFile = await convertVideo(
        videoFilePath,
        folderName,
        fileName
      );
      console.log(`Audio file saved at: ${outputFile}`);
    } catch (err) {
      console.error("An error occurred:", err.message);
    }
  });

We can see the options by running the following command:

cli-tool help convert

Output:

Usage: cli-tool convert [options] <videoFilePath>

Convert a video file to an MP3 audio file

Options:
  -f, --folder <folderName>  Output folder name (default: "converted")
  -n, --name <fileName>      Output file name (without extension) (default: "audio")
  -h, --help                 display help for command
➜  cli

Let's test it out by converting our previously downloaded video by running this command:

cli-tool convert videos/test.mp4 -f audio -n test

Great. We did it. Great job and a high five.

Conclusion

I hope you had as much fun as I did furing this tutorial.

I will make sure to share the repo with the finished code.

simple-cli-tutorial