@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 +72 -0
- package/bin/yt-cli.js +276 -0
- package/install.ps1 +36 -0
- package/install.sh +49 -0
- package/package.json +27 -0
- package/src/search.js +60 -0
- package/src/storage.js +40 -0
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
|
+
[](https://badge.fury.io/js/yt-cli)
|
|
9
|
+
[](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 };
|