Add files via upload

This commit is contained in:
Sudo Space 2024-06-14 20:58:06 +03:30 committed by GitHub
parent 942a3ec9db
commit 7605760bb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 583 additions and 0 deletions

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "lollipop",
"version": "dev",
"main": "app.js",
"keywords": [],
"author": "sudospace",
"license": "MIT",
"description": "lollipop is a friendly and lovely cli youtube downloader for you.",
"devDependencies": {
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^20.14.2",
"@yao-pkg/pkg": "^5.12.0",
"nodemon": "^3.1.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"dependencies": {
"chalk": "^4.1.2",
"commander": "^12.1.0",
"easy-table": "^1.2.0",
"fluent-ffmpeg": "^2.1.3",
"moment": "^2.30.1",
"ytdl-core": "^4.11.5"
},
"scripts": {
"start": "node dist/app.js",
"dev": "nodemon src/app.ts",
"build": "tsc",
"build-w": "tsc -w",
"make-linux-x64": "npm run build && pkg --targets node20-linux-x64 --compress GZip dist/app.js -o lollipop-linux-x64",
"make-linux-arm64": "npm run build && pkg --targets node20-linux-arm64 --compress GZip dist/app.js -o lollipop-linux-arm64",
"make-win-x64": "npm run build && pkg --targets node20-win-x64 --compress GZip dist/app.js -o lollipop-win-x64",
"make-win-arm64": "npm run build && pkg --targets node20-win-arm64 --compress GZip dist/app.js -o lollipop-win-arm64",
"make-macos-x64": "npm run build && pkg --targets node20-macos-x64 --compress GZip dist/app.js -o lollipop-macos-x64",
"make-macos-arm64": "npm run build && pkg --targets node20-macos-arm64 --compress GZip dist/app.js -o lollipop-macos-arm64"
}
}

124
src/app.ts Normal file
View file

@ -0,0 +1,124 @@
import chalk from "chalk";
import { Command, Option } from "commander";
import {
downloadVideo,
linkInfomation,
checkLink,
isTagValid,
downloadAudio,
merging,
conevrtToMp3,
debug,
} from "./cli/functions";
import { Wrong } from "./cli/logs";
const app = new Command();
app
.name("lollipop")
.description(
`A ${chalk.cyan.bold.underline("friendly")} and ${chalk
.hex("#F075AA")
.bold.underline("lovely")} cli youtube downloader for you ${chalk.hex(
"#D04848"
)("<3")}`
)
.version(`0.0.1`, "--version")
.usage("[command]")
.option("--debug", "simple debugger, It's a joke XD")
.addOption(new Option("-h, --help").hideHelp());
app
.command("get")
.description("show youtube link details for you :3")
.argument("<link>", "youtube link")
.addOption(new Option("-h, --help").hideHelp())
.addHelpText(
"after",
`${chalk.green("\nExamples:")}
${chalk.yellow("get")} youtube_link`
)
.action(async (link, options) => {
if (options.debug) {
debug.enable = true;
}
checkLink(link);
linkInfomation(link);
});
app
.command("down")
.description("download youtube videos/audios for you :P")
.argument("<link>", "youtube link")
.option("-v <tag>", "pass the video tag you got from the get command")
.option("-a <tag>", "pass the audio tag you got from the get command")
.option("--mp3", "this flag is only used together with -a flag")
.addOption(new Option("-h, --help").hideHelp())
.addHelpText(
"after",
`${chalk.green("\nExamples:")}
standard download => ${chalk.yellow(
"down"
)} youtube_link -v tag_number -a tag_number
${chalk.hex("#DC84F3")(
`In standard download, I download video and audio separately and merging them with ffmpeg`
)}\n
download only video => ${chalk.yellow("down")} youtube_link -v tag_number\n
download only audio => ${chalk.yellow(
"down"
)} youtube_link -a tag_number --mp3
${chalk.hex("#DC84F3")(
`If you use ${chalk.blue("--mp3")} with ${chalk.blue(
"-a"
)} flag, It will convert audio to mp3 with ffmpeg`
)}`
)
.action(async (link, options) => {
if (options.debug) {
debug.enable = true;
}
checkLink(link);
if (options.v && options.a) {
console.log("Please wait for tag validation...");
const isVideoTagValid = await isTagValid(link, options.v);
const isAudioTagValid = await isTagValid(link, options.a);
if (isVideoTagValid && isAudioTagValid) {
const video = await downloadVideo(link, options.v);
const audio = await downloadAudio(link, options.a);
if (options.mp3) {
audio.path = await conevrtToMp3(audio);
}
merging(video, audio);
} else {
if (!isVideoTagValid) {
Wrong.videoTagNotFound();
} else if (!isAudioTagValid) {
Wrong.audioTagNotFound();
}
}
} else if (options.v) {
console.log("Please wait for tag validation...");
const isVideoTagValid = await isTagValid(link, options.v);
if (isVideoTagValid) {
console.log("I will download only video...");
downloadVideo(link, options.v);
} else {
Wrong.videoTagNotFound();
}
} else if (options.a) {
console.log("Please wait for tag validation...");
const isAudioTagValid = await isTagValid(link, options.a);
if (isAudioTagValid) {
console.log("I will download only audio...");
const audio = await downloadAudio(link, options.a);
if (options.mp3) {
conevrtToMp3(audio);
}
} else {
Wrong.audioTagNotFound();
}
}
});
app.parse();

216
src/cli/functions.ts Normal file
View file

@ -0,0 +1,216 @@
import ytdl from "ytdl-core";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs";
import readline from "readline";
import { join, dirname } from "path";
import {
drawAudioQualityTable,
drawVideoQualityTable,
drawBasicTable,
} from "./tables";
import { Successful, Wrong } from "./logs";
import { secondstoTime, kbToSize } from "../utils/ulits";
import { IVideoObject, IAudioObject } from "./interface";
export const debug = {
enable: false,
};
export async function linkInfomation(link: string) {
try {
const info = await ytdl.getInfo(link);
drawBasicTable(info);
drawVideoQualityTable(info.formats);
drawAudioQualityTable(info.formats);
} catch (err) {
Wrong.internet(err, debug.enable);
process.exit(1);
}
}
export async function downloadVideo(link: string, tag: string) {
let info: ytdl.videoInfo;
try {
info = await ytdl.getInfo(link);
} catch (err) {
Wrong.internet(err, debug.enable);
process.exit(1);
}
const title = info.videoDetails.title;
let extension = "";
let quality = "";
let codec = "";
console.log("I am detecting video...");
info.formats.forEach((item) => {
if (item.itag == +tag) {
extension = item.container;
quality = item.qualityLabel;
codec = item.codecs;
}
});
const path = join(
dirname(process.execPath),
`${title}-${quality}-${codec}-video.${extension}`
);
const tracker = {
start: Date.now(),
video: { downloaded: 0, total: Infinity },
};
return new Promise<IVideoObject>((resolve, reject) => {
ytdl(link, { filter: (format) => format.itag == +tag })
.on("progress", (_, downloaded, total) => {
tracker.video = { downloaded, total };
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(
`Downloading video: ${kbToSize(
tracker.video.downloaded
)} / ${kbToSize(tracker.video.total)}`
);
})
.on("finish", () => {
Successful.videoDownloaded();
const time = `Average video download time: ${secondstoTime(
(Date.now() - tracker.start) / 1000
)}`;
console.log(time);
resolve({ path, title, extension, quality, codec });
})
.on("error", (err) => {
Wrong.errorOnVideoBuffers(err, debug.enable);
process.exit(1);
})
.pipe(fs.createWriteStream(path));
});
}
export async function downloadAudio(link: string, tag: string) {
let info: ytdl.videoInfo;
try {
info = await ytdl.getInfo(link);
} catch (err) {
Wrong.internet(err, debug.enable);
process.exit(1);
}
const title = info.videoDetails.title;
let extension = "";
let bitrate = 0;
let codec = "";
let channels = 0;
console.log("I am detecting audio...");
info.formats.forEach((item) => {
if (item.itag == +tag) {
extension = item.container;
bitrate = item.audioBitrate!;
codec = item.codecs;
channels = item.audioChannels!;
}
});
const path = join(
dirname(process.execPath),
`${title}-${bitrate}-${codec}-audio.${extension}`
);
const tracker = {
start: Date.now(),
audio: { downloaded: 0, total: Infinity },
};
return new Promise<IAudioObject>((resolve, reject) => {
ytdl(link, { filter: (format) => format.itag == +tag })
.on("progress", (_, downloaded, total) => {
tracker.audio = { downloaded, total };
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(
`Downloaging audio: ${kbToSize(
tracker.audio.downloaded
)} / ${kbToSize(tracker.audio.total)}`
);
})
.on("finish", () => {
Successful.audioDownloaded();
const time = `Average video download time: ${secondstoTime(
(Date.now() - tracker.start) / 1000
)}`;
console.log(time);
resolve({ path, title, codec, extension, channels, bitrate });
})
.on("error", (err) => {
Wrong.errorOnAudioBuffers(err, debug.enable);
process.exit(1);
})
.pipe(fs.createWriteStream(path));
});
}
export function merging(video: IVideoObject, audio: IAudioObject) {
const path = `${video.title}-${video.quality}-${video.codec}.${video.extension}`;
ffmpeg()
.mergeAdd(video.path)
.mergeAdd(audio.path)
.save(join(dirname(process.execPath), path))
.on("progress", () => {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(`Merging video with audio...`);
})
.on("end", () => {
console.log("\nMerging Done :D");
fs.unlinkSync(video.path);
fs.unlinkSync(audio.path);
})
.on("error", (err) => {
Wrong.errorOnMerging(err, debug.enable);
process.exit(1);
});
}
export function conevrtToMp3(audio: IAudioObject) {
const path = `${audio.title}-${audio.bitrate}kb.mp3`;
return new Promise<string>((resolve, reject) => {
ffmpeg()
.input(audio.path)
.format("mp3")
.audioBitrate(audio.bitrate)
.audioChannels(audio.channels)
.save(join(dirname(process.execPath), path))
.on("progress", () => {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(`Converting to mp3...`);
})
.on("end", () => {
console.log("\nAudio converted to mp3 file :D");
fs.unlinkSync(audio.path);
resolve(path);
})
.on("error", (err) => {
Wrong.errorOnAudioBuffers(err, debug.enable);
process.exit(1);
});
});
}
export function checkLink(link: string) {
if (!ytdl.validateURL(link)) {
Wrong.invalidLink();
process.exit(1);
}
}
export async function isTagValid(link: string, tag: string) {
let info: ytdl.videoInfo;
try {
info = await ytdl.getInfo(link);
} catch (err) {
Wrong.internet(err, debug.enable);
process.exit(1);
}
for (const item of info.formats) {
if (item.itag == +tag) {
return true;
}
}
return false;
}

16
src/cli/interface.ts Normal file
View file

@ -0,0 +1,16 @@
export interface IVideoObject {
path: string;
title: string;
extension: string;
quality: string;
codec: string;
}
export interface IAudioObject {
path: string;
title: string;
extension: string;
codec: string;
channels: number;
bitrate: number;
}

83
src/cli/logs.ts Normal file
View file

@ -0,0 +1,83 @@
import chalk from "chalk";
export class Wrong {
static internet(err: any, debug: boolean) {
if (debug) {
console.log(err);
} else {
const output = `${chalk.hex("#F3B95F")(
"T~T"
)} Something wrong. check your ${chalk
.hex("#EE4E4E")
.underline("internet")} connection`;
console.log(output);
}
}
static errorOnMerging(err: any, debug: boolean) {
if (debug) {
console.log(err);
} else {
const output = `${chalk.red(
"Something wrong"
)} in merging to a file ${chalk.cyan("T~T")}`;
console.log(output);
}
}
static errorOnAudioBuffers(err: any, debug: boolean) {
if (debug) {
console.log(err);
} else {
const output = `${chalk.red(
"Something wrong"
)} in write audio buffers in file ${chalk.cyan("T~T")}`;
console.log(output);
}
}
static errorOnVideoBuffers(err: any, debug: boolean) {
if (debug) {
console.log(err);
} else {
const output = `${chalk.red(
"Something wrong"
)} in write video buffer in file ${chalk.cyan("T~T")}`;
console.log(output);
}
}
static invalidLink() {
const output = `${chalk.blue("T~T")} Honey, it seems you given me an ${chalk
.hex("#F3B95F")
.underline("invalid link")}`;
console.log(output);
}
static audioTagNotFound() {
const output = `${chalk.hex("#9BABB8")(
":("
)} I can't find this audio tag. Please use ${chalk.yellow.underline(
"get"
)} command to see available tags`;
console.log(output);
}
static videoTagNotFound() {
const output = `${chalk.hex("#9BABB8")(
":("
)} I can't find this video tag. Please use ${chalk.yellow.underline(
"get"
)} command to see available tags`;
console.log(output);
}
}
export class Successful {
static audioDownloaded() {
const output = `\n${chalk.green("YES!")} ${chalk.hex("#FF9B9B")(
":D"
)} audio download has been done`;
console.log(output);
}
static videoDownloaded() {
const output = `\n${chalk.green("YES!")} ${chalk.hex("#FF9B9B")(
":D"
)} video download has been done`;
console.log(output);
}
}

57
src/cli/tables.ts Normal file
View file

@ -0,0 +1,57 @@
import { videoInfo, videoFormat } from "ytdl-core";
import table from "easy-table";
import chalk from "chalk";
import moment from "moment";
import { secondstoTime, kbToSize } from "../utils/ulits";
export function drawBasicTable(info: videoInfo) {
const title = `${chalk.bold.inverse(" Title ")} ==> ${
info.videoDetails.title
}`;
const length = secondstoTime(+info.videoDetails.lengthSeconds);
const channelName = info.videoDetails.ownerChannelName;
const publishDate = info.videoDetails.publishDate;
const t = new table();
t.cell("Video Length", length);
t.cell("Channel name", channelName);
t.cell("Publish date", moment(publishDate).format("YYYY-MM-DD"));
t.newRow();
console.log("\n" + title);
console.log(t.toString());
}
export function drawVideoQualityTable(formats: videoFormat[]) {
const t = new table();
formats.forEach((i) => {
if (i.hasVideo && i.hasAudio == false) {
t.cell(chalk.cyan("Tag"), chalk.cyan(i.itag));
t.cell(chalk.magenta("Quality"), chalk.magenta(i.qualityLabel));
t.cell(chalk.blue("Format"), chalk.blue(i.container));
t.cell(chalk.green("Codec"), chalk.green(i.videoCodec));
t.cell(chalk.yellow("Size"), chalk.yellow(kbToSize(+i.contentLength)));
t.sort([`${chalk.blue("Format")}|des`]);
t.newRow();
}
});
console.log("Available video qualities");
console.log(t.toString());
}
export async function drawAudioQualityTable(formats: videoFormat[]) {
const t = new table();
formats.forEach((i) => {
if (i.hasAudio) {
t.cell(chalk.cyan("Tag"), chalk.cyan(i.itag));
t.cell(chalk.magenta("Bitrate"), chalk.magenta(i.audioBitrate));
t.cell(chalk.green("Codec"), chalk.green(i.audioCodec));
t.cell(chalk.yellow("Size"), chalk.yellow(kbToSize(+i.contentLength)));
t.sort([`${chalk.magenta("Bitrate")}|des`]);
t.newRow();
}
});
console.log("Available audio qualities");
console.log(t.toString());
}

36
src/utils/ulits.ts Normal file
View file

@ -0,0 +1,36 @@
export function secondstoTime(s: number) {
const hours = Math.floor(s / 3600);
const minutes = Math.floor((s - hours * 3600) / 60);
const seconds = Math.floor(s - hours * 3600 - minutes * 60);
const time =
hours.toString().padStart(2, "0") +
":" +
minutes.toString().padStart(2, "0") +
":" +
seconds.toString().padStart(2, "0");
return time;
}
export function kbToSize(kilobit: number) {
if (Number.isNaN(kilobit)) {
return "Incalculable";
}
const megabytes = kilobit / 1024 / 1024;
if (megabytes >= 1024) {
const gigabyte = megabytes / 1024;
return gigabyte.toFixed(2) + "GB";
}
return megabytes.toFixed(2) + "MB";
}
//? It's for future :)
// export function toSize(kilobyte: number) {
// const megabytes = kilobyte / 1024;
// if (megabytes >= 1024) {
// const gigabyte = megabytes / 1024;
// return gigabyte.toFixed(2) + "GB";
// }
// return megabytes.toFixed(2) + "MB";
// }

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "CommonJS",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"rootDir": "src",
"outDir": "dist",
"removeComments": true
}
}