@hrithick0330k/yt-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ <div align="center">
2
+
3
+ # `yt-cli` ๐Ÿ“บ
4
+ ### The Ultimate Terminal Media Center
5
+
6
+ Launch infinite media streams straight from your terminal. Skip the ads. Build your library. Never leave the command line.
7
+
8
+ [![npm version](https://badge.fury.io/js/yt-cli.svg)](https://badge.fury.io/js/yt-cli)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## ๐Ÿš€ Why yt-cli?
16
+
17
+ `yt-cli` isn't just a search toolโ€”it's a drastically optimized terminal environment that transforms your command line into a highly professional YouTube Jukebox and Video Sandbox.
18
+
19
+ - **๐Ÿ“ป Infinite Radio:** Start a song and let `yt-cli` endlessly queue intelligent YouTube Mixes. The music never stops.
20
+ - **๐Ÿšซ Native SponsorBlock:** We don't ask you if you want ads. They are sliced out and skipped natively so you never hear a sponsorship segment again.
21
+ - **๐Ÿ“š Local Hub Library:** Save endless tracks to your persistent terminal vault (`~/.yt-cli-library.json`), and access them instantly without searching.
22
+ - **๐ŸŽต Auto/Audio-Only Modes:** Want to just vibe while you code? Drop it in background audio mode smoothly.
23
+ - **โฌ‡๏ธ 1-Click Downloads:** Rip any media straight to your PC in one stroke.
24
+ - **๐Ÿ’ป Minimalist & Pristine:** Inspired by tools like `ani-cli`, the entire UI aggressively clears to maintain a distraction-free hacking aesthetic.
25
+
26
+ ---
27
+
28
+ ## ๐Ÿ› ๏ธ Installation
29
+
30
+ ### โšก The 1-Line Installer (Recommended)
31
+ This script will automatically detect your OS, install all required dependencies (`mpv`, `yt-dlp`, `node`), and install `yt-cli` globally for you!
32
+
33
+ **Mac / Linux / WSL:**
34
+ ```bash
35
+ curl -fsSL https://raw.githubusercontent.com/hrithick03/yt-cli/main/install.sh | bash
36
+ ```
37
+
38
+ **Windows (PowerShell):**
39
+ ```powershell
40
+ irm https://raw.githubusercontent.com/hrithick03/yt-cli/main/install.ps1 | iex
41
+ ```
42
+
43
+ ### Manual NPM Install
44
+ If you already have [Node.js](https://nodejs.org/), [yt-dlp](https://github.com/yt-dlp/yt-dlp), and [mpv](https://mpv.io/) natively installed:
45
+ ```bash
46
+ npm install -g @hrithick0330k/yt-cli
47
+ ```
48
+ *(If you are installing locally from source, clone the repo and run `npm install -g .` )*
49
+
50
+ ---
51
+
52
+ ## ๐ŸŽฎ Usage
53
+
54
+ Simply pop open your favorite terminal and type:
55
+ ```bash
56
+ yt-cli
57
+ ```
58
+ The aesthetic main hub will boot up, allowing you to seamlessly dive into your library or search for new videos.
59
+
60
+ **Direct Search (Quick Launch):**
61
+ ```bash
62
+ yt-cli "lofi hip hop radio"
63
+ ```
64
+
65
+ ## ๐Ÿ—บ๏ธ Roadmap & Features
66
+ - [x] SponsorBlock Integration
67
+ - [x] Local Library & Playlists
68
+ - [x] Auto-Fetch Next Recommendations (Infinite Radio)
69
+ - [x] Pristine Terminal Interface (Auto-Clean)
70
+
71
+ ## ๐Ÿ“œ License
72
+ MIT License. Created by you and open-source forever. Do what you want with the code.
package/bin/yt-cli.js ADDED
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require("child_process");
4
+ const inquirer = require("inquirer");
5
+ const chalk = require("chalk");
6
+ const { searchYouTube } = require("../src/search");
7
+ const { getLibrary, saveToLibrary, removeFromLibrary } = require("../src/storage");
8
+
9
+ function printBanner() {
10
+ console.clear();
11
+ console.log(chalk.bold.greenBright(`
12
+ โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—
13
+ โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
14
+ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
15
+ โ•šโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ•šโ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
16
+ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘
17
+ โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•
18
+ `));
19
+ console.log(chalk.gray(`Launch infinite media streams from your terminal. v1.0.0\n`));
20
+ }
21
+
22
+ async function startHub() {
23
+ printBanner();
24
+
25
+ let query = process.argv.slice(2).join(" ");
26
+
27
+ if (query) {
28
+ await handleSearch(query);
29
+ return;
30
+ }
31
+
32
+ const { action } = await inquirer.prompt([
33
+ {
34
+ type: "list",
35
+ name: "action",
36
+ message: "Main Menu:",
37
+ choices: [
38
+ { name: "๐Ÿ” Search YouTube", value: "search" },
39
+ { name: "๐Ÿ“š My Library (Favorites)", value: "library" },
40
+ { name: "โŒ Exit", value: "exit" }
41
+ ]
42
+ }
43
+ ]);
44
+
45
+ if (action === "search") {
46
+ const answer = await inquirer.prompt([{
47
+ type: 'input',
48
+ name: 'query',
49
+ message: 'Enter a YouTube search query:',
50
+ validate: (input) => input.trim() ? true : 'Please enter a search term'
51
+ }]);
52
+ await handleSearch(answer.query);
53
+ } else if (action === "library") {
54
+ await showLibrary();
55
+ } else {
56
+ console.log(chalk.gray("Goodbye."));
57
+ process.exit(0);
58
+ }
59
+ }
60
+
61
+ async function showLibrary() {
62
+ const lib = getLibrary();
63
+ if (lib.favorites.length === 0) {
64
+ console.log(chalk.cyan(`\n[INFO] `) + chalk.white(`Your library is empty. Go search and save some videos!`));
65
+ setTimeout(startHub, 2000);
66
+ return;
67
+ }
68
+
69
+ const choices = lib.favorites.map((song) => ({
70
+ name: `${chalk.bold.cyan(song.title)} ${chalk.dim(`[${song.duration}] by ${song.uploader}`)}`,
71
+ value: song,
72
+ }));
73
+
74
+ choices.push(new inquirer.Separator());
75
+ choices.push({ name: "๐Ÿ”™ Back to Main Menu", value: "back" });
76
+
77
+ const { selectedVideo } = await inquirer.prompt([{
78
+ type: "list",
79
+ name: "selectedVideo",
80
+ message: "Your Favorites:",
81
+ choices: choices,
82
+ pageSize: 15,
83
+ }]);
84
+
85
+ if (selectedVideo === "back") {
86
+ return startHub();
87
+ }
88
+
89
+ await handleVideoSelection(selectedVideo, true);
90
+ }
91
+
92
+ async function handleSearch(query) {
93
+ printBanner();
94
+ console.log(chalk.bold.greenBright(`[1/3] YouTube Resolution`));
95
+ console.log(chalk.gray(`-----------------------------------------------------------------------------------------`));
96
+ console.log(chalk.cyan(`[INFO] `) + chalk.white(`Resolving query: ${query}...`));
97
+
98
+ let results = [];
99
+ try {
100
+ results = await searchYouTube(query, 15);
101
+ } catch (error) {
102
+ console.log(chalk.redBright(`[ERROR] `) + chalk.white(`Failed to search. Verify your yt-dlp installation.`));
103
+ process.exit(1);
104
+ }
105
+
106
+ if (results.length === 0) {
107
+ console.log(chalk.yellowBright(`[WARN] `) + chalk.white(`No results found for your query.`));
108
+ process.exit(1);
109
+ }
110
+
111
+ console.log(chalk.greenBright(` โœ” Search completed. Found ${results.length} valid results.\n`));
112
+
113
+ const choices = results.map((song) => ({
114
+ name: `${chalk.bold.green(song.title)} ${chalk.dim(`[${song.duration}] by ${song.uploader}`)}`,
115
+ value: song,
116
+ }));
117
+
118
+ choices.push(new inquirer.Separator());
119
+ choices.push({ name: "๐Ÿ”™ Back to Main Menu", value: "back" });
120
+
121
+ const { selectedVideo } = await inquirer.prompt([{
122
+ type: "list",
123
+ name: "selectedVideo",
124
+ message: "Select a video:",
125
+ choices: choices,
126
+ pageSize: 15,
127
+ }]);
128
+
129
+ if (selectedVideo === "back") {
130
+ return startHub();
131
+ }
132
+
133
+ await handleVideoSelection(selectedVideo, false);
134
+ }
135
+
136
+ async function handleVideoSelection(video, fromLibrary = false) {
137
+ const { action } = await inquirer.prompt([
138
+ {
139
+ type: "list",
140
+ name: "action",
141
+ message: "What would you like to do?",
142
+ choices: [
143
+ { name: "โ–ถ๏ธ Play Video / Audio", value: "play" },
144
+ { name: "๐Ÿ“ป Start Infinite Radio", value: "radio" },
145
+ { name: "โฌ‡๏ธ Download to PC", value: "download" },
146
+ fromLibrary
147
+ ? { name: "๐Ÿ—‘๏ธ Remove from Library", value: "remove" }
148
+ : { name: "โค๏ธ Save to Library", value: "save" },
149
+ { name: "๐Ÿ”™ Back", value: "back" }
150
+ ]
151
+ }
152
+ ]);
153
+
154
+ if (action === "save") {
155
+ const saved = saveToLibrary(video);
156
+ if (saved) console.log(chalk.bold.greenBright(`\n โœ” Saved to your library!`));
157
+ else console.log(chalk.cyan(`\n[INFO] `) + chalk.white(`Already in your library.`));
158
+ setTimeout(startHub, 1500);
159
+ return;
160
+ }
161
+
162
+ if (action === "remove") {
163
+ removeFromLibrary(video.id);
164
+ console.log(chalk.bold.greenBright(`\n โœ” Removed from library.`));
165
+ setTimeout(() => showLibrary(), 1500);
166
+ return;
167
+ }
168
+
169
+ if (action === "back") {
170
+ if (fromLibrary) return showLibrary();
171
+ return startHub();
172
+ }
173
+
174
+ if (action === "download") {
175
+ downloadVideo(video, fromLibrary);
176
+ return;
177
+ }
178
+
179
+ const { playbackOptions } = await inquirer.prompt([
180
+ {
181
+ type: "list",
182
+ name: "playbackOptions",
183
+ message: "Select quality / format:",
184
+ choices: [
185
+ { name: "๐ŸŽฌ Auto (Best Quality)", value: "bestvideo+bestaudio/best" },
186
+ { name: "๐ŸŽฌ 1080p", value: "bestvideo[height<=1080]+bestaudio/best" },
187
+ { name: "๐ŸŽฌ 720p", value: "bestvideo[height<=720]+bestaudio/best" },
188
+ { name: "๐ŸŽต Audio Only (Background)", value: "bestaudio/best" },
189
+ ]
190
+ }
191
+ ]);
192
+
193
+ playVideo(video, playbackOptions, true, action === "radio", fromLibrary);
194
+ }
195
+
196
+ function downloadVideo(video, fromLibrary) {
197
+ printBanner();
198
+ console.log(chalk.bold.greenBright(`[2/3] Downloading Media`));
199
+ console.log(chalk.gray(`-----------------------------------------------------------------------------------------`));
200
+ console.log(chalk.cyan(`[INFO] `) + chalk.white(`Target: ${video.title}`));
201
+ console.log(chalk.cyan(`[INFO] `) + chalk.white(`Starting yt-dlp fetch...\n`));
202
+
203
+ const ydlpArgs = [video.url, "-o", "%(title)s.%(ext)s"];
204
+ const dl = spawn("yt-dlp", ydlpArgs, { stdio: "inherit" });
205
+
206
+ dl.on("close", (code) => {
207
+ if (code === 0) console.log(chalk.bold.greenBright(`\n โœ” Download completed successfully!`));
208
+ else console.log(chalk.bold.redBright(`\n[ERROR] `) + chalk.white(`Download failed or interrupted.`));
209
+ showPostMenu(fromLibrary);
210
+ });
211
+ }
212
+
213
+ function playVideo(video, formatStr, skipSponsors, isRadio, fromLibrary) {
214
+ printBanner();
215
+ console.log(chalk.bold.greenBright(`[2/3] Launching Media Sandbox`));
216
+ console.log(chalk.gray(`-----------------------------------------------------------------------------------------`));
217
+ console.log(chalk.cyan(`[INFO] `) + chalk.white(`Target Title: ${video.title}`));
218
+ if (isRadio) console.log(chalk.cyan(`[INFO] `) + chalk.greenBright(`Mode: Infinite Radio Stream ACTIVE`));
219
+ if (skipSponsors) console.log(chalk.cyan(`[INFO] `) + chalk.white(`SponsorBlock Engine: ENABLED`));
220
+ console.log(chalk.cyan(`[INFO] `) + chalk.white(`Handing off control to MPV...\n`));
221
+
222
+ // For infinite radio, use the YouTube Auto-generated Mix RD[id]
223
+ const targetUrl = isRadio ? `https://www.youtube.com/watch?v=${video.id}&list=RD${video.id}` : video.url;
224
+
225
+ const mpvArgs = [
226
+ targetUrl,
227
+ `--ytdl-format=${formatStr}`,
228
+ "--really-quiet",
229
+ "--title=" + video.title,
230
+ ];
231
+
232
+ if (formatStr.includes("bestaudio") && !formatStr.includes("bestvideo")) {
233
+ mpvArgs.push("--no-video");
234
+ }
235
+
236
+ if (skipSponsors) {
237
+ mpvArgs.push("--ytdl-raw-options=sponsorblock-remove=sponsor");
238
+ }
239
+
240
+ const mpv = spawn("mpv", mpvArgs, { stdio: "inherit" });
241
+
242
+ mpv.on("close", (code) => {
243
+ console.log(chalk.cyan(`\n[INFO] `) + chalk.white(`Sandbox session closed. (Exit Code: ${code})`));
244
+ showPostMenu(fromLibrary);
245
+ });
246
+ }
247
+
248
+ function showPostMenu(fromLibrary) {
249
+ inquirer.prompt([{
250
+ type: "list",
251
+ name: "action",
252
+ message: "What next?",
253
+ choices: [
254
+ { name: "๐Ÿ  Back to Main Menu", value: "hub" },
255
+ { name: "โŒ Quit", value: "quit" }
256
+ ]
257
+ }]).then((answers) => {
258
+ if (answers.action === "hub") {
259
+ process.argv = process.argv.slice(0, 2); // reset args for clean hub
260
+ startHub();
261
+ } else {
262
+ process.exit(0);
263
+ }
264
+ });
265
+ }
266
+
267
+ // Ignore initial arguments on unhandled rejections
268
+ process.on("unhandledRejection", (r) => {
269
+ console.log(chalk.red("Terminated."));
270
+ process.exit();
271
+ });
272
+
273
+ // Start app
274
+ startHub().catch((err) => {
275
+ console.error(chalk.red("\nAn error occurred: " + err.message));
276
+ });
package/install.ps1 ADDED
@@ -0,0 +1,36 @@
1
+ # yt-cli One-Line Installer Script for Windows
2
+ Write-Host ""
3
+ Write-Host "โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—" -ForegroundColor Green
4
+ Write-Host "โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘" -ForegroundColor Green
5
+ Write-Host " โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘" -ForegroundColor Green
6
+ Write-Host " โ•šโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ•šโ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘" -ForegroundColor Green
7
+ Write-Host " โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘" -ForegroundColor Green
8
+ Write-Host " โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•" -ForegroundColor Green
9
+ Write-Host ""
10
+ Write-Host "Initializing yt-cli installation..." -ForegroundColor Cyan
11
+
12
+ # Install dependencies via Winget if available
13
+ Write-Host "[INFO] Checking dependencies (Node.js, MPV, yt-dlp)..." -ForegroundColor Cyan
14
+ $wingetPath = Get-Command winget -ErrorAction SilentlyContinue
15
+
16
+ if ($null -ne $wingetPath) {
17
+ winget install --id=Nodejs.Nodejs -e --accept-package-agreements --accept-source-agreements
18
+ winget install --id=mpv.mpv -e --accept-package-agreements --accept-source-agreements
19
+ winget install --id=yt-dlp.yt-dlp -e --accept-package-agreements --accept-source-agreements
20
+ } else {
21
+ Write-Host "[WARN] winget is not installed. Please install Node.js, mpv, and yt-dlp manually." -ForegroundColor Yellow
22
+ }
23
+
24
+ # Clone and install globally
25
+ Write-Host "[INFO] Fetching yt-cli source code and installing globally..." -ForegroundColor Cyan
26
+ $tempDir = Join-Path $env:TEMP "yt-cli-install"
27
+ if (Test-Path $tempDir) { Remove-Item -Recurse -Force $tempDir }
28
+ git clone https://github.com/hrithick03/yt-cli.git $tempDir
29
+ Set-Location $tempDir
30
+ npm install -g .
31
+
32
+ Write-Host ""
33
+ Write-Host " โœ” Installation Complete! " -ForegroundColor Green -NoNewline
34
+ Write-Host "You can now run " -NoNewline
35
+ Write-Host "yt-cli " -ForegroundColor Magenta -NoNewline
36
+ Write-Host "from your terminal."
package/install.sh ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # yt-cli One-Line Installer Script
3
+ set -e
4
+
5
+ echo -e "\033[1;32m"
6
+ echo "โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—"
7
+ echo "โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘"
8
+ echo " โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘"
9
+ echo " โ•šโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ•šโ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘"
10
+ echo " โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘"
11
+ echo " โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•"
12
+ echo -e "\033[0m"
13
+ echo "Initializing yt-cli installation..."
14
+
15
+ # Install dependencies based on OS
16
+ if [[ "$OSTYPE" == "darwin"* ]]; then
17
+ # macOS
18
+ echo "[INFO] macOS detected. Checking required dependencies..."
19
+ if ! command -v brew &> /dev/null; then
20
+ echo "[ERROR] Homebrew is required but not installed."
21
+ exit 1
22
+ fi
23
+ brew install mpv yt-dlp node
24
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
25
+ # Linux
26
+ echo "[INFO] Linux detected. Checking required dependencies..."
27
+ if command -v apt &> /dev/null; then
28
+ sudo apt update
29
+ sudo apt install -y mpv yt-dlp nodejs npm
30
+ elif command -v pacman &> /dev/null; then
31
+ sudo pacman -Sy --noconfirm mpv yt-dlp nodejs npm
32
+ else
33
+ echo "[WARN] Unsupported package manager. Please install mpv, yt-dlp, and nodejs manually."
34
+ fi
35
+ else
36
+ echo "[WARN] Unsupported OS for this script. Please install manually."
37
+ fi
38
+
39
+ # Clone and install yt-cli
40
+ echo "[INFO] Fetching yt-cli source code..."
41
+ TMP_DIR=$(mktemp -d)
42
+ git clone https://github.com/hrithick03/yt-cli.git "$TMP_DIR"
43
+ cd "$TMP_DIR"
44
+
45
+ echo "[INFO] Installing yt-cli globally via NPM..."
46
+ sudo npm install -g .
47
+
48
+ echo -e "\n\033[1;32m โœ” Installation Complete! \033[0m"
49
+ echo "You can now run yt-cli from anywhere by typing: yt-cli"
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@hrithick0330k/yt-cli",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool to search and play videos from YouTube",
5
+ "main": "src/search.js",
6
+ "bin": {
7
+ "yt-cli": "bin/yt-cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/yt-cli.js"
11
+ },
12
+ "keywords": [
13
+ "music",
14
+ "youtube",
15
+ "cli",
16
+ "player",
17
+ "mpv",
18
+ "yt-dlp",
19
+ "terminal"
20
+ ],
21
+ "author": "hrith",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "inquirer": "^8.2.6",
25
+ "chalk": "^4.1.2"
26
+ }
27
+ }
package/src/search.js ADDED
@@ -0,0 +1,60 @@
1
+ const { spawn } = require("child_process");
2
+
3
+ function searchYouTube(query, limit = 10) {
4
+ return new Promise((resolve, reject) => {
5
+ // Run yt-dlp to output JSON metadata
6
+ // --dump-json outputs a strict JSON object per line.
7
+ const args = [
8
+ `ytsearch${limit}:${query}`,
9
+ "--dump-json",
10
+ "--no-playlist",
11
+ "--default-search", "ytsearch"
12
+ ];
13
+
14
+ const ytdlp = spawn("yt-dlp", args);
15
+
16
+ let output = "";
17
+ let errorOutput = "";
18
+
19
+ ytdlp.stdout.on("data", (data) => {
20
+ output += data.toString();
21
+ });
22
+
23
+ ytdlp.stderr.on("data", (data) => {
24
+ errorOutput += data.toString();
25
+ });
26
+
27
+ ytdlp.on("close", (code) => {
28
+ if (code !== 0 && !output.trim()) {
29
+ console.error("yt-dlp error:", errorOutput);
30
+ return reject(new Error("Failed to fetch search results from yt-dlp."));
31
+ }
32
+
33
+ // Parse JSON lines
34
+ const songs = [];
35
+ const lines = output.trim().split("\n");
36
+ for (const line of lines) {
37
+ if (!line.trim()) continue;
38
+ try {
39
+ const v = JSON.parse(line);
40
+ songs.push({
41
+ id: v.id,
42
+ title: v.title,
43
+ uploader: v.uploader || "Unknown",
44
+ duration: v.duration_string || "Unknown",
45
+ url: v.webpage_url || `https://www.youtube.com/watch?v=${v.id}`
46
+ });
47
+ } catch (e) {
48
+ // skip invalid json line (could be a warning if pushed to stdout stream)
49
+ }
50
+ }
51
+ resolve(songs);
52
+ });
53
+
54
+ ytdlp.on("error", (err) => {
55
+ reject(err);
56
+ });
57
+ });
58
+ }
59
+
60
+ module.exports = { searchYouTube };
package/src/storage.js ADDED
@@ -0,0 +1,40 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const libraryPath = path.join(os.homedir(), '.yt-cli-library.json');
6
+
7
+ function initLibrary() {
8
+ if (!fs.existsSync(libraryPath)) {
9
+ fs.writeFileSync(libraryPath, JSON.stringify({ favorites: [] }, null, 2));
10
+ }
11
+ }
12
+
13
+ function getLibrary() {
14
+ initLibrary();
15
+ try {
16
+ const data = fs.readFileSync(libraryPath, 'utf8');
17
+ return JSON.parse(data);
18
+ } catch (error) {
19
+ return { favorites: [] };
20
+ }
21
+ }
22
+
23
+ function saveToLibrary(video) {
24
+ const lib = getLibrary();
25
+ // check if already exists
26
+ if (!lib.favorites.find(v => v.id === video.id)) {
27
+ lib.favorites.push(video);
28
+ fs.writeFileSync(libraryPath, JSON.stringify(lib, null, 2));
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ function removeFromLibrary(videoId) {
35
+ const lib = getLibrary();
36
+ lib.favorites = lib.favorites.filter(v => v.id !== videoId);
37
+ fs.writeFileSync(libraryPath, JSON.stringify(lib, null, 2));
38
+ }
39
+
40
+ module.exports = { getLibrary, saveToLibrary, removeFromLibrary };