@involvex/youtube-music-cli 0.0.25 → 0.0.27
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/CHANGELOG.md +4 -0
- package/dist/source/cli.js +109 -8
- package/dist/source/components/layouts/SearchLayout.js +13 -5
- package/dist/source/main.js +61 -16
- package/dist/source/services/player/dependency-check.service.d.ts +12 -0
- package/dist/source/services/player/dependency-check.service.js +140 -0
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +1 -1
- package/readme.md +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## [0.0.27](https://github.com/involvex/youtube-music-cli/compare/v0.0.26...v0.0.27) (2026-02-20)
|
|
2
|
+
|
|
3
|
+
## [0.0.26](https://github.com/involvex/youtube-music-cli/compare/v0.0.25...v0.0.26) (2026-02-20)
|
|
4
|
+
|
|
1
5
|
## [0.0.25](https://github.com/involvex/youtube-music-cli/compare/v0.0.24...v0.0.25) (2026-02-20)
|
|
2
6
|
|
|
3
7
|
### Features
|
package/dist/source/cli.js
CHANGED
|
@@ -11,7 +11,10 @@ import { getWebServerManager } from "./services/web/web-server-manager.js";
|
|
|
11
11
|
import { getWebStreamingService } from "./services/web/web-streaming.service.js";
|
|
12
12
|
import { getVersionCheckService } from "./services/version-check/version-check.service.js";
|
|
13
13
|
import { getConfigService } from "./services/config/config.service.js";
|
|
14
|
+
import { getPlayerService } from "./services/player/player.service.js";
|
|
14
15
|
import { APP_VERSION } from "./utils/constants.js";
|
|
16
|
+
import { ensurePlaybackDependencies } from "./services/player/dependency-check.service.js";
|
|
17
|
+
import { getMusicService } from "./services/youtube-music/api.js";
|
|
15
18
|
const cli = meow(`
|
|
16
19
|
Usage
|
|
17
20
|
$ youtube-music-cli
|
|
@@ -117,6 +120,64 @@ if (cli.flags.help) {
|
|
|
117
120
|
// Handle plugin commands
|
|
118
121
|
const command = cli.input[0];
|
|
119
122
|
const args = cli.input.slice(1);
|
|
123
|
+
const isInteractiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
124
|
+
function requiresImmediatePlayback(flags) {
|
|
125
|
+
return Boolean(flags.playTrack || flags.searchQuery || flags.playPlaylist);
|
|
126
|
+
}
|
|
127
|
+
function shouldCheckPlaybackDependencies(commandName, flags) {
|
|
128
|
+
if (flags.webOnly) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (requiresImmediatePlayback(flags)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return (commandName === undefined ||
|
|
135
|
+
commandName === 'suggestions' ||
|
|
136
|
+
Boolean(flags.web));
|
|
137
|
+
}
|
|
138
|
+
async function runDirectPlaybackCommand(flags) {
|
|
139
|
+
const musicService = getMusicService();
|
|
140
|
+
const playerService = getPlayerService();
|
|
141
|
+
const config = getConfigService();
|
|
142
|
+
const playbackOptions = {
|
|
143
|
+
volume: flags.volume ?? config.get('volume'),
|
|
144
|
+
audioNormalization: config.get('audioNormalization'),
|
|
145
|
+
};
|
|
146
|
+
let track;
|
|
147
|
+
if (flags.playTrack) {
|
|
148
|
+
track = await musicService.getTrack(flags.playTrack);
|
|
149
|
+
if (!track) {
|
|
150
|
+
throw new Error(`Track not found: ${flags.playTrack}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (flags.searchQuery) {
|
|
154
|
+
const response = await musicService.search(flags.searchQuery, {
|
|
155
|
+
type: 'songs',
|
|
156
|
+
limit: 1,
|
|
157
|
+
});
|
|
158
|
+
const firstSong = response.results.find(result => result.type === 'song');
|
|
159
|
+
if (!firstSong) {
|
|
160
|
+
throw new Error(`No playable tracks found for: "${flags.searchQuery}"`);
|
|
161
|
+
}
|
|
162
|
+
track = firstSong.data;
|
|
163
|
+
}
|
|
164
|
+
else if (flags.playPlaylist) {
|
|
165
|
+
const playlist = await musicService.getPlaylist(flags.playPlaylist);
|
|
166
|
+
track = playlist.tracks[0];
|
|
167
|
+
if (!track) {
|
|
168
|
+
throw new Error(`No playable tracks found in playlist: ${flags.playPlaylist}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!track) {
|
|
172
|
+
throw new Error('No track resolved for playback command.');
|
|
173
|
+
}
|
|
174
|
+
const artists = track.artists.length > 0
|
|
175
|
+
? track.artists.map(artist => artist.name).join(', ')
|
|
176
|
+
: 'Unknown Artist';
|
|
177
|
+
console.log(`Playing: ${track.title} — ${artists}`);
|
|
178
|
+
const youtubeUrl = `https://www.youtube.com/watch?v=${track.videoId}`;
|
|
179
|
+
await playerService.play(youtubeUrl, playbackOptions);
|
|
180
|
+
}
|
|
120
181
|
if (command === 'plugins') {
|
|
121
182
|
const subCommand = args[0];
|
|
122
183
|
const pluginArg = args[1];
|
|
@@ -274,6 +335,28 @@ else {
|
|
|
274
335
|
else if (command === 'back') {
|
|
275
336
|
cli.flags.action = 'previous';
|
|
276
337
|
}
|
|
338
|
+
const flags = cli.flags;
|
|
339
|
+
const shouldRunDirectPlayback = requiresImmediatePlayback(flags) &&
|
|
340
|
+
(flags.headless || !isInteractiveTerminal);
|
|
341
|
+
if (shouldRunDirectPlayback) {
|
|
342
|
+
void (async () => {
|
|
343
|
+
const dependencyCheck = await ensurePlaybackDependencies({
|
|
344
|
+
interactive: isInteractiveTerminal,
|
|
345
|
+
});
|
|
346
|
+
if (!dependencyCheck.ready) {
|
|
347
|
+
process.exit(1);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
await runDirectPlaybackCommand(flags);
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
console.error(`✗ Playback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
}
|
|
277
360
|
else if (command === 'import') {
|
|
278
361
|
// Handle import commands
|
|
279
362
|
void (async () => {
|
|
@@ -317,6 +400,15 @@ else {
|
|
|
317
400
|
void (async () => {
|
|
318
401
|
const webManager = getWebServerManager();
|
|
319
402
|
try {
|
|
403
|
+
if (shouldCheckPlaybackDependencies(command, flags)) {
|
|
404
|
+
const dependencyCheck = await ensurePlaybackDependencies({
|
|
405
|
+
interactive: isInteractiveTerminal,
|
|
406
|
+
});
|
|
407
|
+
if (!dependencyCheck.ready && requiresImmediatePlayback(flags)) {
|
|
408
|
+
process.exit(1);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
320
412
|
await webManager.start({
|
|
321
413
|
enabled: true,
|
|
322
414
|
host: cli.flags.webHost ?? 'localhost',
|
|
@@ -343,7 +435,7 @@ else {
|
|
|
343
435
|
}
|
|
344
436
|
else {
|
|
345
437
|
// Also render the CLI UI
|
|
346
|
-
render(_jsx(App, { flags:
|
|
438
|
+
render(_jsx(App, { flags: flags }));
|
|
347
439
|
}
|
|
348
440
|
}
|
|
349
441
|
catch (error) {
|
|
@@ -353,9 +445,18 @@ else {
|
|
|
353
445
|
})();
|
|
354
446
|
}
|
|
355
447
|
else {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
448
|
+
void (async () => {
|
|
449
|
+
if (shouldCheckPlaybackDependencies(command, flags)) {
|
|
450
|
+
const dependencyCheck = await ensurePlaybackDependencies({
|
|
451
|
+
interactive: isInteractiveTerminal,
|
|
452
|
+
});
|
|
453
|
+
if (!dependencyCheck.ready && requiresImmediatePlayback(flags)) {
|
|
454
|
+
process.exit(1);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Check for updates before rendering the app (skip in web-only mode)
|
|
459
|
+
if (!cli.flags.webOnly) {
|
|
359
460
|
const versionCheck = getVersionCheckService();
|
|
360
461
|
const config = getConfigService();
|
|
361
462
|
const lastCheck = config.getLastVersionCheck();
|
|
@@ -369,9 +470,9 @@ else {
|
|
|
369
470
|
console.log('');
|
|
370
471
|
}
|
|
371
472
|
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
473
|
+
}
|
|
474
|
+
// Render the app
|
|
475
|
+
render(_jsx(App, { flags: flags }));
|
|
476
|
+
})();
|
|
376
477
|
}
|
|
377
478
|
}
|
|
@@ -19,6 +19,7 @@ function SearchLayout() {
|
|
|
19
19
|
const [isSearching, setIsSearching] = useState(false);
|
|
20
20
|
const [actionMessage, setActionMessage] = useState(null);
|
|
21
21
|
const actionTimeoutRef = useRef(null);
|
|
22
|
+
const lastAutoSearchedQueryRef = useRef(null);
|
|
22
23
|
// Handle search action
|
|
23
24
|
const performSearch = useCallback(async (query) => {
|
|
24
25
|
if (!query || isSearching)
|
|
@@ -56,12 +57,18 @@ function SearchLayout() {
|
|
|
56
57
|
useKeyBinding(['h'], goToHistory);
|
|
57
58
|
// Initial search if query is in state (usually from CLI flags)
|
|
58
59
|
useEffect(() => {
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
const query = navState.searchQuery.trim();
|
|
61
|
+
if (!query || navState.hasSearched) {
|
|
62
|
+
return;
|
|
61
63
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (lastAutoSearchedQueryRef.current === query) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
lastAutoSearchedQueryRef.current = query;
|
|
68
|
+
queueMicrotask(() => {
|
|
69
|
+
void performSearch(query);
|
|
70
|
+
});
|
|
71
|
+
}, [navState.searchQuery, navState.hasSearched, performSearch]);
|
|
65
72
|
// Handle going back
|
|
66
73
|
const goBack = useCallback(() => {
|
|
67
74
|
if (!isTyping) {
|
|
@@ -106,6 +113,7 @@ function SearchLayout() {
|
|
|
106
113
|
setResults([]);
|
|
107
114
|
dispatch({ category: 'SET_HAS_SEARCHED', hasSearched: false });
|
|
108
115
|
dispatch({ category: 'SET_SEARCH_QUERY', query: '' });
|
|
116
|
+
lastAutoSearchedQueryRef.current = null;
|
|
109
117
|
};
|
|
110
118
|
}, [dispatch]);
|
|
111
119
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Limit: ", navState.searchLimit, " (Use [ or ] to adjust)"] }), _jsx(SearchBar, { isActive: isTyping && !isSearching, onInput: input => {
|
package/dist/source/main.js
CHANGED
|
@@ -57,23 +57,68 @@ function Initializer({ flags }) {
|
|
|
57
57
|
}
|
|
58
58
|
function HeadlessLayout({ flags }) {
|
|
59
59
|
const { play, pause, resume, next, previous } = usePlayer();
|
|
60
|
-
const { getTrack } = useYouTubeMusic();
|
|
60
|
+
const { getTrack, getPlaylist, search } = useYouTubeMusic();
|
|
61
61
|
useEffect(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
void (async () => {
|
|
63
|
+
if (flags?.playTrack) {
|
|
64
|
+
const track = await getTrack(flags.playTrack);
|
|
65
|
+
if (!track) {
|
|
66
|
+
console.error(`Track not found: ${flags.playTrack}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
play(track);
|
|
71
|
+
console.log(`Playing: ${track.title}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (flags?.searchQuery) {
|
|
75
|
+
const response = await search(flags.searchQuery, {
|
|
76
|
+
type: 'songs',
|
|
77
|
+
limit: 1,
|
|
78
|
+
});
|
|
79
|
+
const songResult = response?.results.find(result => result.type === 'song');
|
|
80
|
+
if (!songResult) {
|
|
81
|
+
console.error(`No playable tracks found for: "${flags.searchQuery}"`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const track = songResult.data;
|
|
86
|
+
play(track, { clearQueue: true });
|
|
87
|
+
console.log(`Playing: ${track.title}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (flags?.playPlaylist) {
|
|
91
|
+
const playlist = await getPlaylist(flags.playPlaylist);
|
|
92
|
+
const firstTrack = playlist?.tracks[0];
|
|
93
|
+
if (!firstTrack) {
|
|
94
|
+
console.error(`No playable tracks found in playlist: ${flags.playPlaylist}`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
play(firstTrack, { clearQueue: true });
|
|
99
|
+
console.log(`Playing playlist "${playlist.name}": ${firstTrack.title}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (flags?.action === 'pause')
|
|
103
|
+
pause();
|
|
104
|
+
if (flags?.action === 'resume')
|
|
105
|
+
resume();
|
|
106
|
+
if (flags?.action === 'next')
|
|
107
|
+
next();
|
|
108
|
+
if (flags?.action === 'previous')
|
|
109
|
+
previous();
|
|
110
|
+
})();
|
|
111
|
+
}, [
|
|
112
|
+
flags,
|
|
113
|
+
play,
|
|
114
|
+
pause,
|
|
115
|
+
resume,
|
|
116
|
+
next,
|
|
117
|
+
previous,
|
|
118
|
+
getTrack,
|
|
119
|
+
getPlaylist,
|
|
120
|
+
search,
|
|
121
|
+
]);
|
|
77
122
|
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "green", children: "Headless mode active." }) }));
|
|
78
123
|
}
|
|
79
124
|
export default function Main({ flags }) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PlaybackDependency = 'mpv' | 'yt-dlp';
|
|
2
|
+
export type InstallPlan = {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function buildInstallPlan(platform: NodeJS.Platform, availableManagers: readonly string[], missingDependencies: readonly PlaybackDependency[]): InstallPlan | null;
|
|
7
|
+
export declare function ensurePlaybackDependencies(options: {
|
|
8
|
+
interactive: boolean;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
ready: boolean;
|
|
11
|
+
missing: PlaybackDependency[];
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
const REQUIRED_DEPENDENCIES = ['mpv', 'yt-dlp'];
|
|
4
|
+
function getDependencyExecutable(dependency) {
|
|
5
|
+
if (process.platform === 'win32') {
|
|
6
|
+
return dependency === 'mpv' ? 'mpv.exe' : 'yt-dlp.exe';
|
|
7
|
+
}
|
|
8
|
+
return dependency;
|
|
9
|
+
}
|
|
10
|
+
function renderInstallCommand(plan) {
|
|
11
|
+
return [plan.command, ...plan.args].join(' ');
|
|
12
|
+
}
|
|
13
|
+
function runCommand(command, args, options) {
|
|
14
|
+
return new Promise(resolve => {
|
|
15
|
+
const child = spawn(command, args, {
|
|
16
|
+
stdio: options.stdio,
|
|
17
|
+
});
|
|
18
|
+
child.once('error', () => {
|
|
19
|
+
resolve(false);
|
|
20
|
+
});
|
|
21
|
+
child.once('close', code => {
|
|
22
|
+
resolve(code === 0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function commandExists(command) {
|
|
27
|
+
return runCommand(command, ['--version'], { stdio: 'ignore' });
|
|
28
|
+
}
|
|
29
|
+
async function getMissingDependencies() {
|
|
30
|
+
const missing = [];
|
|
31
|
+
for (const dependency of REQUIRED_DEPENDENCIES) {
|
|
32
|
+
const executable = getDependencyExecutable(dependency);
|
|
33
|
+
const exists = await commandExists(executable);
|
|
34
|
+
if (!exists) {
|
|
35
|
+
missing.push(dependency);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return missing;
|
|
39
|
+
}
|
|
40
|
+
export function buildInstallPlan(platform, availableManagers, missingDependencies) {
|
|
41
|
+
if (missingDependencies.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const deps = [...missingDependencies];
|
|
45
|
+
const hasManager = (manager) => availableManagers.includes(manager);
|
|
46
|
+
if ((platform === 'darwin' || platform === 'linux') && hasManager('brew')) {
|
|
47
|
+
return { command: 'brew', args: ['install', ...deps] };
|
|
48
|
+
}
|
|
49
|
+
if (platform === 'win32') {
|
|
50
|
+
if (hasManager('scoop')) {
|
|
51
|
+
return { command: 'scoop', args: ['install', ...deps] };
|
|
52
|
+
}
|
|
53
|
+
if (hasManager('choco')) {
|
|
54
|
+
return { command: 'choco', args: ['install', ...deps, '-y'] };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (platform === 'linux') {
|
|
59
|
+
if (hasManager('apt-get')) {
|
|
60
|
+
return {
|
|
61
|
+
command: 'sudo',
|
|
62
|
+
args: ['apt-get', 'install', '-y', ...deps],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (hasManager('pacman')) {
|
|
66
|
+
return {
|
|
67
|
+
command: 'sudo',
|
|
68
|
+
args: ['pacman', '-S', '--needed', ...deps],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (hasManager('dnf')) {
|
|
72
|
+
return {
|
|
73
|
+
command: 'sudo',
|
|
74
|
+
args: ['dnf', 'install', '-y', ...deps],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
async function getAvailablePackageManagers(platform) {
|
|
81
|
+
const candidates = platform === 'win32'
|
|
82
|
+
? ['scoop', 'choco']
|
|
83
|
+
: platform === 'darwin'
|
|
84
|
+
? ['brew']
|
|
85
|
+
: ['brew', 'apt-get', 'pacman', 'dnf'];
|
|
86
|
+
const available = [];
|
|
87
|
+
for (const manager of candidates) {
|
|
88
|
+
if (await commandExists(manager)) {
|
|
89
|
+
available.push(manager);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return available;
|
|
93
|
+
}
|
|
94
|
+
function printManualInstallHelp(missing, plan) {
|
|
95
|
+
console.error(`\nMissing playback dependencies: ${missing.join(', ')}. Install them and re-run the command.`);
|
|
96
|
+
if (plan) {
|
|
97
|
+
console.error(`Suggested install command: ${renderInstallCommand(plan)}\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.error('Suggested install commands:\n macOS: brew install mpv yt-dlp\n Windows: scoop install mpv yt-dlp\n Linux (apt): sudo apt-get install -y mpv yt-dlp\n');
|
|
101
|
+
}
|
|
102
|
+
export async function ensurePlaybackDependencies(options) {
|
|
103
|
+
const missing = await getMissingDependencies();
|
|
104
|
+
if (missing.length === 0) {
|
|
105
|
+
return { ready: true, missing: [] };
|
|
106
|
+
}
|
|
107
|
+
const availableManagers = await getAvailablePackageManagers(process.platform);
|
|
108
|
+
const installPlan = buildInstallPlan(process.platform, availableManagers, missing);
|
|
109
|
+
if (!options.interactive || !installPlan) {
|
|
110
|
+
printManualInstallHelp(missing, installPlan);
|
|
111
|
+
return { ready: false, missing };
|
|
112
|
+
}
|
|
113
|
+
const prompt = `Missing ${missing.join(', ')}. Install now with "${renderInstallCommand(installPlan)}"? [Y/n] `;
|
|
114
|
+
const readline = createInterface({
|
|
115
|
+
input: process.stdin,
|
|
116
|
+
output: process.stdout,
|
|
117
|
+
});
|
|
118
|
+
const response = (await readline.question(prompt)).trim().toLowerCase();
|
|
119
|
+
readline.close();
|
|
120
|
+
if (response === 'n' || response === 'no') {
|
|
121
|
+
printManualInstallHelp(missing, installPlan);
|
|
122
|
+
return { ready: false, missing };
|
|
123
|
+
}
|
|
124
|
+
console.log(`\nInstalling dependencies: ${missing.join(', ')}`);
|
|
125
|
+
const installSuccess = await runCommand(installPlan.command, installPlan.args, {
|
|
126
|
+
stdio: 'inherit',
|
|
127
|
+
});
|
|
128
|
+
if (!installSuccess) {
|
|
129
|
+
console.error('\nAutomatic installation failed.');
|
|
130
|
+
printManualInstallHelp(missing, installPlan);
|
|
131
|
+
return { ready: false, missing };
|
|
132
|
+
}
|
|
133
|
+
const missingAfterInstall = await getMissingDependencies();
|
|
134
|
+
if (missingAfterInstall.length > 0) {
|
|
135
|
+
printManualInstallHelp(missingAfterInstall, installPlan);
|
|
136
|
+
return { ready: false, missing: missingAfterInstall };
|
|
137
|
+
}
|
|
138
|
+
console.log('Playback dependencies installed successfully.\n');
|
|
139
|
+
return { ready: true, missing: [] };
|
|
140
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -46,9 +46,6 @@ scoop install mpv yt-dlp
|
|
|
46
46
|
|
|
47
47
|
# With Chocolatey
|
|
48
48
|
choco install mpv yt-dlp
|
|
49
|
-
|
|
50
|
-
# With winget
|
|
51
|
-
winget install mpv yt-dlp
|
|
52
49
|
```
|
|
53
50
|
|
|
54
51
|
</details>
|
|
@@ -99,13 +96,23 @@ bun install -g @involvex/youtube-music-cli
|
|
|
99
96
|
brew install involvex/youtube-music-cli/youtube-music-cli
|
|
100
97
|
```
|
|
101
98
|
|
|
102
|
-
###
|
|
99
|
+
### GitHub Releases
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
https://github.com/involvex/youtube-music-cli/releases
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Install Script (bash)
|
|
103
106
|
|
|
104
107
|
```bash
|
|
105
|
-
|
|
108
|
+
curl -fssl https://raw.githubusercontent.com/involvex/youtube-music-cli/main/scripts/install.sh | bash
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
|
|
111
|
+
### Install Script (PowerShell)
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
iwr https://raw.githubusercontent.com/involvex/youtube-music-cli/main/scripts/install.ps1 | iex
|
|
115
|
+
```
|
|
109
116
|
|
|
110
117
|
### From Source
|
|
111
118
|
|
|
@@ -315,6 +322,8 @@ Ensure mpv is installed and in your PATH:
|
|
|
315
322
|
mpv --version
|
|
316
323
|
```
|
|
317
324
|
|
|
325
|
+
On startup, the CLI now checks for `mpv` and `yt-dlp`. In interactive terminals it can prompt to run an install command automatically (with explicit confirmation first).
|
|
326
|
+
|
|
318
327
|
### No audio
|
|
319
328
|
|
|
320
329
|
1. Check volume isn't muted (`=` to increase)
|