@involvex/youtube-music-cli 0.0.44 → 0.0.46
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 +12 -0
- package/dist/package.json +1 -1
- package/dist/source/cli.js +1 -0
- package/dist/source/components/settings/Settings.js +26 -13
- package/dist/source/services/config/config.service.js +1 -0
- package/dist/source/services/player/player.service.d.ts +1 -0
- package/dist/source/services/player/player.service.js +5 -0
- package/dist/source/services/web/web-server-manager.js +3 -0
- package/dist/source/stores/player.store.js +42 -7
- package/dist/source/types/config.types.d.ts +1 -0
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
- package/readme.md +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.0.46](https://github.com/involvex/youtube-music-cli/compare/v0.0.45...v0.0.46) (2026-02-24)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- keep autoplay running and document completions ([9356c5d](https://github.com/involvex/youtube-music-cli/commit/9356c5db6583cd926a78af15302cdac5bde65aab))
|
|
6
|
+
|
|
7
|
+
## [0.0.45](https://github.com/involvex/youtube-music-cli/compare/v0.0.44...v0.0.45) (2026-02-24)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- add volume fade duration setting for smoother playback transitions ([936e3e2](https://github.com/involvex/youtube-music-cli/commit/936e3e2c7ae043b26dd48a1cd6f2d26cb11b0ae2))
|
|
12
|
+
|
|
1
13
|
## [0.0.44](https://github.com/involvex/youtube-music-cli/compare/v0.0.43...v0.0.44) (2026-02-23)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/dist/package.json
CHANGED
package/dist/source/cli.js
CHANGED
|
@@ -157,6 +157,7 @@ async function runDirectPlaybackCommand(flags) {
|
|
|
157
157
|
const playbackOptions = {
|
|
158
158
|
volume: flags.volume ?? config.get('volume'),
|
|
159
159
|
audioNormalization: config.get('audioNormalization'),
|
|
160
|
+
volumeFadeDuration: config.get('volumeFadeDuration'),
|
|
160
161
|
};
|
|
161
162
|
let track;
|
|
162
163
|
if (flags.playTrack) {
|
|
@@ -21,11 +21,13 @@ const EQUALIZER_PRESETS = [
|
|
|
21
21
|
'bright',
|
|
22
22
|
'warm',
|
|
23
23
|
];
|
|
24
|
+
const VOLUME_FADE_PRESETS = [0, 1, 2, 3, 5];
|
|
24
25
|
const SETTINGS_ITEMS = [
|
|
25
26
|
'Stream Quality',
|
|
26
27
|
'Audio Normalization',
|
|
27
28
|
'Gapless Playback',
|
|
28
29
|
'Crossfade Duration',
|
|
30
|
+
'Volume Fade Duration',
|
|
29
31
|
'Equalizer Preset',
|
|
30
32
|
'Notifications',
|
|
31
33
|
'Discord Rich Presence',
|
|
@@ -47,6 +49,7 @@ export default function Settings() {
|
|
|
47
49
|
const [audioNormalization, setAudioNormalization] = useState(config.get('audioNormalization') ?? false);
|
|
48
50
|
const [gaplessPlayback, setGaplessPlayback] = useState(config.get('gaplessPlayback') ?? true);
|
|
49
51
|
const [crossfadeDuration, setCrossfadeDuration] = useState(config.get('crossfadeDuration') ?? 0);
|
|
52
|
+
const [volumeFadeDuration, setVolumeFadeDuration] = useState(config.get('volumeFadeDuration') ?? 0);
|
|
50
53
|
const [equalizerPreset, setEqualizerPreset] = useState(config.get('equalizerPreset') ?? 'flat');
|
|
51
54
|
const [notifications, setNotifications] = useState(config.get('notifications') ?? false);
|
|
52
55
|
const [discordRpc, setDiscordRpc] = useState(config.get('discordRichPresence') ?? false);
|
|
@@ -85,6 +88,13 @@ export default function Settings() {
|
|
|
85
88
|
setCrossfadeDuration(next);
|
|
86
89
|
config.set('crossfadeDuration', next);
|
|
87
90
|
};
|
|
91
|
+
const cycleVolumeFadeDuration = () => {
|
|
92
|
+
const currentIndex = VOLUME_FADE_PRESETS.indexOf(volumeFadeDuration);
|
|
93
|
+
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % VOLUME_FADE_PRESETS.length;
|
|
94
|
+
const next = VOLUME_FADE_PRESETS[nextIndex] ?? 0;
|
|
95
|
+
setVolumeFadeDuration(next);
|
|
96
|
+
config.set('volumeFadeDuration', next);
|
|
97
|
+
};
|
|
88
98
|
const cycleEqualizerPreset = () => {
|
|
89
99
|
const currentIndex = EQUALIZER_PRESETS.indexOf(equalizerPreset);
|
|
90
100
|
const nextPreset = EQUALIZER_PRESETS[(currentIndex + 1) % EQUALIZER_PRESETS.length];
|
|
@@ -142,36 +152,39 @@ export default function Settings() {
|
|
|
142
152
|
cycleCrossfadeDuration();
|
|
143
153
|
}
|
|
144
154
|
else if (selectedIndex === 4) {
|
|
145
|
-
|
|
155
|
+
cycleVolumeFadeDuration();
|
|
146
156
|
}
|
|
147
157
|
else if (selectedIndex === 5) {
|
|
148
|
-
|
|
158
|
+
cycleEqualizerPreset();
|
|
149
159
|
}
|
|
150
160
|
else if (selectedIndex === 6) {
|
|
151
|
-
|
|
161
|
+
toggleNotifications();
|
|
152
162
|
}
|
|
153
163
|
else if (selectedIndex === 7) {
|
|
154
|
-
|
|
164
|
+
toggleDiscordRpc();
|
|
155
165
|
}
|
|
156
166
|
else if (selectedIndex === 8) {
|
|
157
|
-
|
|
167
|
+
toggleDownloadsEnabled();
|
|
158
168
|
}
|
|
159
169
|
else if (selectedIndex === 9) {
|
|
160
|
-
|
|
170
|
+
setIsEditingDownloadDirectory(true);
|
|
161
171
|
}
|
|
162
172
|
else if (selectedIndex === 10) {
|
|
163
|
-
|
|
173
|
+
cycleDownloadFormat();
|
|
164
174
|
}
|
|
165
175
|
else if (selectedIndex === 11) {
|
|
166
|
-
|
|
176
|
+
cycleSleepTimer();
|
|
167
177
|
}
|
|
168
178
|
else if (selectedIndex === 12) {
|
|
169
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.
|
|
179
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
|
|
170
180
|
}
|
|
171
181
|
else if (selectedIndex === 13) {
|
|
172
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.
|
|
182
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.EXPORT_PLAYLISTS });
|
|
173
183
|
}
|
|
174
184
|
else if (selectedIndex === 14) {
|
|
185
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.KEYBINDINGS });
|
|
186
|
+
}
|
|
187
|
+
else if (selectedIndex === 15) {
|
|
175
188
|
dispatch({ category: 'NAVIGATE', view: VIEW.PLUGINS });
|
|
176
189
|
}
|
|
177
190
|
};
|
|
@@ -181,7 +194,7 @@ export default function Settings() {
|
|
|
181
194
|
const sleepTimerLabel = isActive && remainingSeconds !== null
|
|
182
195
|
? `Sleep Timer: ${formatTime(remainingSeconds)} remaining (Enter to cancel)`
|
|
183
196
|
: 'Sleep Timer: Off (Enter to set)';
|
|
184
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Settings" }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 0 ? theme.colors.primary : undefined, color: selectedIndex === 0 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 0, children: ["Stream Quality: ", quality.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 1 ? theme.colors.primary : undefined, color: selectedIndex === 1 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 1, children: ["Audio Normalization: ", audioNormalization ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 2 ? theme.colors.primary : undefined, color: selectedIndex === 2 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 2, children: ["Gapless Playback: ", gaplessPlayback ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 3 ? theme.colors.primary : undefined, color: selectedIndex === 3 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 3, children: ["Crossfade: ", crossfadeDuration === 0 ? 'Off' : `${crossfadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 4 ? theme.colors.primary : undefined, color: selectedIndex === 4 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 4, children: ["Equalizer: ", formatEqualizerLabel(equalizerPreset)] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex ===
|
|
197
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Settings" }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 0 ? theme.colors.primary : undefined, color: selectedIndex === 0 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 0, children: ["Stream Quality: ", quality.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 1 ? theme.colors.primary : undefined, color: selectedIndex === 1 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 1, children: ["Audio Normalization: ", audioNormalization ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 2 ? theme.colors.primary : undefined, color: selectedIndex === 2 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 2, children: ["Gapless Playback: ", gaplessPlayback ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 3 ? theme.colors.primary : undefined, color: selectedIndex === 3 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 3, children: ["Crossfade: ", crossfadeDuration === 0 ? 'Off' : `${crossfadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 4 ? theme.colors.primary : undefined, color: selectedIndex === 4 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 4, children: ["Volume Fade:", ' ', volumeFadeDuration === 0 ? 'Off' : `${volumeFadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 5 ? theme.colors.primary : undefined, color: selectedIndex === 5 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 5, children: ["Equalizer: ", formatEqualizerLabel(equalizerPreset)] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 6 ? theme.colors.primary : undefined, color: selectedIndex === 6 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 6, children: ["Desktop Notifications: ", notifications ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 7 ? theme.colors.primary : undefined, color: selectedIndex === 7 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 7, children: ["Discord Rich Presence: ", discordRpc ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 8 ? theme.colors.primary : undefined, color: selectedIndex === 8 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 8, children: ["Download Feature: ", downloadsEnabled ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: isEditingDownloadDirectory && selectedIndex === 9 ? (_jsx(TextInput, { value: downloadDirectory, onChange: setDownloadDirectory, onSubmit: value => {
|
|
185
198
|
const normalized = value.trim();
|
|
186
199
|
if (!normalized) {
|
|
187
200
|
setIsEditingDownloadDirectory(false);
|
|
@@ -190,9 +203,9 @@ export default function Settings() {
|
|
|
190
203
|
setDownloadDirectory(normalized);
|
|
191
204
|
config.set('downloadDirectory', normalized);
|
|
192
205
|
setIsEditingDownloadDirectory(false);
|
|
193
|
-
}, placeholder: "Download directory", focus: true })) : (_jsxs(Text, { backgroundColor: selectedIndex ===
|
|
206
|
+
}, placeholder: "Download directory", focus: true })) : (_jsxs(Text, { backgroundColor: selectedIndex === 9 ? theme.colors.primary : undefined, color: selectedIndex === 9 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 9, children: ["Download Folder: ", downloadDirectory] })) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 10 ? theme.colors.primary : undefined, color: selectedIndex === 10 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 10, children: ["Download Format: ", downloadFormat.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 11 ? theme.colors.primary : undefined, color: selectedIndex === 11
|
|
194
207
|
? theme.colors.background
|
|
195
208
|
: isActive
|
|
196
209
|
? theme.colors.accent
|
|
197
|
-
: theme.colors.text, bold: selectedIndex ===
|
|
210
|
+
: theme.colors.text, bold: selectedIndex === 11, children: sleepTimerLabel }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 12 ? theme.colors.primary : undefined, color: selectedIndex === 12 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 12, children: "Import Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 13 ? theme.colors.primary : undefined, color: selectedIndex === 13 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 13, children: "Export Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 14 ? theme.colors.primary : undefined, color: selectedIndex === 14 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 14, children: "Custom Keybindings \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 15 ? theme.colors.primary : undefined, color: selectedIndex === 15 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 15, children: "Manage Plugins" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Arrows to navigate, Enter to select, Esc/q to go back" }) })] }));
|
|
198
211
|
}
|
|
@@ -5,11 +5,16 @@ import { logger } from "../logger/logger.service.js";
|
|
|
5
5
|
export function buildMpvArgs(url, ipcPath, options) {
|
|
6
6
|
const gapless = options.gaplessPlayback ?? true;
|
|
7
7
|
const crossfadeDuration = Math.max(0, options.crossfadeDuration ?? 0);
|
|
8
|
+
const fadeDuration = Math.max(0, options.volumeFadeDuration ?? 0);
|
|
8
9
|
const eqPreset = options.equalizerPreset ?? 'flat';
|
|
9
10
|
const audioFilters = [];
|
|
10
11
|
if (options.audioNormalization) {
|
|
11
12
|
audioFilters.push('dynaudnorm');
|
|
12
13
|
}
|
|
14
|
+
if (fadeDuration > 0) {
|
|
15
|
+
audioFilters.push(`afade=t=in:st=0:d=${fadeDuration}`);
|
|
16
|
+
audioFilters.push(`afade=t=out:d=${fadeDuration}`);
|
|
17
|
+
}
|
|
13
18
|
if (crossfadeDuration > 0) {
|
|
14
19
|
audioFilters.push(`acrossfade=d=${crossfadeDuration}`);
|
|
15
20
|
}
|
|
@@ -141,6 +141,7 @@ class WebServerManager {
|
|
|
141
141
|
handleCommand(action) {
|
|
142
142
|
logger.debug('WebServerManager', 'Executing command from client', { action });
|
|
143
143
|
const playerService = getPlayerService();
|
|
144
|
+
const config = getConfigService();
|
|
144
145
|
// Execute command and update internal state
|
|
145
146
|
switch (action.category) {
|
|
146
147
|
case 'PLAY': {
|
|
@@ -152,6 +153,7 @@ class WebServerManager {
|
|
|
152
153
|
const youtubeUrl = `https://www.youtube.com/watch?v=${action.track.videoId}`;
|
|
153
154
|
void playerService.play(youtubeUrl, {
|
|
154
155
|
volume: this.internalState.volume,
|
|
156
|
+
volumeFadeDuration: config.get('volumeFadeDuration'),
|
|
155
157
|
});
|
|
156
158
|
}
|
|
157
159
|
break;
|
|
@@ -202,6 +204,7 @@ class WebServerManager {
|
|
|
202
204
|
const youtubeUrl = `https://www.youtube.com/watch?v=${this.internalState.currentTrack.videoId}`;
|
|
203
205
|
void playerService.play(youtubeUrl, {
|
|
204
206
|
volume: this.internalState.volume,
|
|
207
|
+
volumeFadeDuration: config.get('volumeFadeDuration'),
|
|
205
208
|
});
|
|
206
209
|
}
|
|
207
210
|
break;
|
|
@@ -251,6 +251,7 @@ function PlayerManager() {
|
|
|
251
251
|
}, [dispatch]);
|
|
252
252
|
// Register event handler for mpv IPC events
|
|
253
253
|
const eofTimestampRef = useRef(0);
|
|
254
|
+
const lastAutoNextRef = useRef(0);
|
|
254
255
|
useEffect(() => {
|
|
255
256
|
let lastProgressUpdate = 0;
|
|
256
257
|
const PROGRESS_THROTTLE_MS = 1000; // Update progress max once per second
|
|
@@ -269,8 +270,10 @@ function PlayerManager() {
|
|
|
269
270
|
if (event.eof) {
|
|
270
271
|
// Track ended — record timestamp so we can suppress mpv's spurious
|
|
271
272
|
// pause event that immediately follows EOF (idle state).
|
|
272
|
-
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
eofTimestampRef.current = now;
|
|
273
275
|
next();
|
|
276
|
+
lastAutoNextRef.current = now;
|
|
274
277
|
}
|
|
275
278
|
if (event.paused !== undefined) {
|
|
276
279
|
// mpv sends pause=true when a track ends and it enters idle mode.
|
|
@@ -402,6 +405,7 @@ function PlayerManager() {
|
|
|
402
405
|
gaplessPlayback: config.get('gaplessPlayback') ?? true,
|
|
403
406
|
crossfadeDuration: config.get('crossfadeDuration') ?? 0,
|
|
404
407
|
equalizerPreset: config.get('equalizerPreset') ?? 'flat',
|
|
408
|
+
volumeFadeDuration: config.get('volumeFadeDuration') ?? 0,
|
|
405
409
|
});
|
|
406
410
|
logger.info('PlayerManager', 'Playback started successfully', {
|
|
407
411
|
attempt,
|
|
@@ -504,14 +508,45 @@ function PlayerManager() {
|
|
|
504
508
|
config.set('volume', state.volume);
|
|
505
509
|
}, [state.volume]);
|
|
506
510
|
// Handle track completion
|
|
511
|
+
const autoAdvanceRef = useRef(false);
|
|
507
512
|
useEffect(() => {
|
|
508
|
-
if (state.duration
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
+
if (state.duration <= 0) {
|
|
514
|
+
autoAdvanceRef.current = false;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (state.progress < state.duration) {
|
|
518
|
+
autoAdvanceRef.current = false;
|
|
519
|
+
return;
|
|
513
520
|
}
|
|
514
|
-
|
|
521
|
+
if (state.repeat === 'one') {
|
|
522
|
+
dispatch({ category: 'SEEK', position: 0 });
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const hasNextTrack = state.queue.length > 0 &&
|
|
526
|
+
(state.repeat === 'all' ||
|
|
527
|
+
state.queuePosition < state.queue.length - 1 ||
|
|
528
|
+
(state.shuffle && state.queue.length > 1));
|
|
529
|
+
if (!hasNextTrack) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const now = Date.now();
|
|
533
|
+
if (now - lastAutoNextRef.current < 1500) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (!autoAdvanceRef.current) {
|
|
537
|
+
autoAdvanceRef.current = true;
|
|
538
|
+
lastAutoNextRef.current = now;
|
|
539
|
+
dispatch({ category: 'NEXT' });
|
|
540
|
+
}
|
|
541
|
+
}, [
|
|
542
|
+
state.duration,
|
|
543
|
+
state.progress,
|
|
544
|
+
state.repeat,
|
|
545
|
+
state.queue.length,
|
|
546
|
+
state.queuePosition,
|
|
547
|
+
state.shuffle,
|
|
548
|
+
dispatch,
|
|
549
|
+
]);
|
|
515
550
|
return null;
|
|
516
551
|
}
|
|
517
552
|
export function PlayerProvider({ children }) {
|
package/dist/youtube-music-cli
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -27,6 +27,7 @@ A powerful Terminal User Interface (TUI) music player for YouTube Music
|
|
|
27
27
|
- 🖥️ **Headless Mode** - Run without TUI for scripting
|
|
28
28
|
- 💾 **Downloads** - Save tracks/playlists/artists with `Shift+D`
|
|
29
29
|
- 🏷️ **Metadata Tagging** - Auto-tag title/artist/album with optional cover art
|
|
30
|
+
- ⚡️ **Shell Completions** - `ymc completions <bash|zsh|powershell|fish>` emits scripts you can source or save so the CLI (also available as `ymc`) tab-completes subcommands and flags
|
|
30
31
|
|
|
31
32
|
## Roadmap
|
|
32
33
|
|
|
@@ -161,6 +162,28 @@ youtube-music-cli skip
|
|
|
161
162
|
youtube-music-cli back
|
|
162
163
|
```
|
|
163
164
|
|
|
165
|
+
### Shell completions
|
|
166
|
+
|
|
167
|
+
Generate shell completion helpers through the lightweight `ymc` alias that ships with the CLI. Run `ymc completions <bash|zsh|powershell|fish>` to print the completion script for your shell, then source it or persist it in your profile:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Bash
|
|
171
|
+
source <(ymc completions bash)
|
|
172
|
+
ymc completions bash >> ~/.bash_completion
|
|
173
|
+
|
|
174
|
+
# Zsh
|
|
175
|
+
source <(ymc completions zsh)
|
|
176
|
+
|
|
177
|
+
# PowerShell
|
|
178
|
+
ymc completions powershell | Out-File -Encoding utf8 $PROFILE
|
|
179
|
+
Invoke-Expression (ymc completions powershell)
|
|
180
|
+
|
|
181
|
+
# Fish
|
|
182
|
+
ymc completions fish > ~/.config/fish/completions/ymc.fish
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If you installed the CLI globally with an alias or script name, make sure `ymc` points at the same binary before generating completions so that the script matches your install path.
|
|
186
|
+
|
|
164
187
|
### Options
|
|
165
188
|
|
|
166
189
|
| Flag | Short | Description |
|