@involvex/youtube-music-cli 0.0.24 → 0.0.25
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 +6 -0
- package/dist/source/components/common/Help.js +10 -2
- package/dist/source/components/common/ShortcutsBar.js +2 -2
- package/dist/source/components/layouts/MainLayout.js +25 -1
- package/dist/source/main.js +1 -10
- package/dist/source/services/player/player.service.js +9 -1
- package/dist/source/utils/constants.d.ts +1 -0
- package/dist/source/utils/constants.js +1 -0
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +1 -1
- package/readme.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.0.25](https://github.com/involvex/youtube-music-cli/compare/v0.0.24...v0.0.25) (2026-02-20)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add Shift+Q/R shortcuts and improve help view navigation ([0d68ad0](https://github.com/involvex/youtube-music-cli/commit/0d68ad0f6aa2e379b164a4b35a452a592e2ae421))
|
|
6
|
+
|
|
1
7
|
## [0.0.24](https://github.com/involvex/youtube-music-cli/compare/v0.0.23...v0.0.24) (2026-02-20)
|
|
2
8
|
|
|
3
9
|
### Features
|
|
@@ -3,8 +3,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { useTheme } from "../../hooks/useTheme.js";
|
|
5
5
|
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
6
|
+
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
7
|
+
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
8
|
+
import { useCallback } from 'react';
|
|
6
9
|
export default function Help() {
|
|
7
10
|
const { theme } = useTheme();
|
|
8
|
-
const { dispatch
|
|
9
|
-
|
|
11
|
+
const { dispatch } = useNavigation();
|
|
12
|
+
const closeHelp = useCallback(() => {
|
|
13
|
+
dispatch({ category: 'GO_BACK' });
|
|
14
|
+
}, [dispatch]);
|
|
15
|
+
useKeyBinding(KEYBINDINGS.BACK, closeHelp);
|
|
16
|
+
useKeyBinding(KEYBINDINGS.SELECT, closeHelp);
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, padding: 1, children: [_jsx(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Keyboard Shortcuts" }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Global" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "q" }), " - Quit", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "?" }), " - Help", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "/" }), " - Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Shift+P" }), " - Playlists", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "p" }), " - Plugins", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "g" }), " - Suggestions", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "," }), " - Settings", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Shift+Q" }), " - Background Play", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Shift+R" }), " - Resume Control"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Player" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Space" }), " - Play/Pause", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "n" }), " - Next", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "b" }), " - Previous", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "=" }), " - Volume Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "-" }), " - Volume Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Shift+S" }), " - Toggle Shuffle", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "r" }), " - Toggle Repeat"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Navigation" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Up" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "k" }), " - Move Up", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Down" }), " /", _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: "j" }), " - Move Down", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Enter" }), " - Select", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Go Back"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Search" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "Tab" }), " - Switch Search Type", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "m" }), " - Create Mix Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Shift+D" }), " - Download selection", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " - Clear Search", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "[ / ]" }), " - Results Limit"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Playlist" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "a" }), " - Add to Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "d" }), " - Remove from Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "c" }), " - Create Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Shift+D" }), " - Download Playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "D" }), " - Delete Playlist"] }) }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "View" }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.text, children: "M" }), " - Toggle Mini Player", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "l" }), " - Lyrics", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "T" }), " - Trending", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "e" }), " - Explore"] }) }), _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "Esc" }), ",", ' ', _jsx(Text, { color: theme.colors.text, children: "Enter" }), ",", ' ', _jsx(Text, { color: theme.colors.text, children: "q" }), ", or", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " to close"] })] })] }));
|
|
10
18
|
}
|
|
@@ -27,7 +27,7 @@ export default function ShortcutsBar() {
|
|
|
27
27
|
useKeyBinding(KEYBINDINGS.SHUFFLE, toggleShuffle);
|
|
28
28
|
useKeyBinding(KEYBINDINGS.REPEAT, toggleRepeat);
|
|
29
29
|
// Note: SETTINGS keybinding handled by MainLayout to avoid double-dispatch
|
|
30
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.text, children: "\
|
|
30
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.text, children: "\u23EF [Space]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u23EE [B/\u2190]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u23ED [N/\u2192]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\uD83D\uDD00 [Shift+S]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\uD83D\uDD01 [R]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\uD83D\uDCDA [Shift+P]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2B07 [Shift+D]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\uD83D\uDD0E [/]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2753 [?]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\uD83D\uDEF0 [Shift+Q]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\uD83D\uDD0C [Shift+R]" }), " |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u23FB [Q]" })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: playerState.shuffle ? theme.colors.primary : theme.colors.dim, children: "\uD83D\uDD00" }), ' ', _jsx(Text, { color: playerState.repeat === 'off'
|
|
31
31
|
? theme.colors.dim
|
|
32
|
-
: theme.colors.secondary, children: playerState.repeat === 'one' ? '🔂' : '🔄' }), ' ', _jsx(Text, { color: theme.colors.dim, children: "\uD83D\uDD0A [
|
|
32
|
+
: theme.colors.secondary, children: playerState.repeat === 'one' ? '🔂' : '🔄' }), ' ', _jsx(Text, { color: theme.colors.dim, children: "\uD83D\uDD0A [=/-]" }), ' ', _jsxs(Text, { color: theme.colors.primary, children: [playerState.volume, "%"] })] })] }));
|
|
33
33
|
}
|
|
@@ -27,9 +27,11 @@ import { Box } from 'ink';
|
|
|
27
27
|
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
28
28
|
import { getPlayerService } from "../../services/player/player.service.js";
|
|
29
29
|
import { getConfigService } from "../../services/config/config.service.js";
|
|
30
|
+
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
30
31
|
function MainLayout() {
|
|
31
32
|
const { theme } = useTheme();
|
|
32
33
|
const { state: navState, dispatch } = useNavigation();
|
|
34
|
+
const { resume } = usePlayer();
|
|
33
35
|
const { columns } = useTerminalSize();
|
|
34
36
|
// Responsive padding based on terminal size
|
|
35
37
|
const getPadding = () => (columns < 100 ? 0 : 1);
|
|
@@ -50,8 +52,12 @@ function MainLayout() {
|
|
|
50
52
|
dispatch({ category: 'NAVIGATE', view: VIEW.SETTINGS });
|
|
51
53
|
}, [dispatch]);
|
|
52
54
|
const goToHelp = useCallback(() => {
|
|
55
|
+
if (navState.currentView === VIEW.HELP) {
|
|
56
|
+
dispatch({ category: 'GO_BACK' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
53
59
|
dispatch({ category: 'NAVIGATE', view: VIEW.HELP });
|
|
54
|
-
}, [dispatch]);
|
|
60
|
+
}, [dispatch, navState.currentView]);
|
|
55
61
|
const handleQuit = useCallback(() => {
|
|
56
62
|
// From player view, quit the app
|
|
57
63
|
if (navState.currentView === VIEW.PLAYER) {
|
|
@@ -91,6 +97,23 @@ function MainLayout() {
|
|
|
91
97
|
// Exit the app
|
|
92
98
|
process.exit(0);
|
|
93
99
|
}, []);
|
|
100
|
+
const handleResumeBackground = useCallback(() => {
|
|
101
|
+
const player = getPlayerService();
|
|
102
|
+
const config = getConfigService();
|
|
103
|
+
const backgroundState = config.getBackgroundPlaybackState();
|
|
104
|
+
if (!backgroundState.enabled || !backgroundState.ipcPath) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
void player
|
|
108
|
+
.reattach(backgroundState.ipcPath)
|
|
109
|
+
.then(() => {
|
|
110
|
+
resume();
|
|
111
|
+
config.clearBackgroundPlaybackState();
|
|
112
|
+
})
|
|
113
|
+
.catch(() => {
|
|
114
|
+
config.clearBackgroundPlaybackState();
|
|
115
|
+
});
|
|
116
|
+
}, [resume]);
|
|
94
117
|
const togglePlayerMode = useCallback(() => {
|
|
95
118
|
dispatch({ category: 'TOGGLE_PLAYER_MODE' });
|
|
96
119
|
}, [dispatch]);
|
|
@@ -108,6 +131,7 @@ function MainLayout() {
|
|
|
108
131
|
useKeyBinding(['e'], goToExplore);
|
|
109
132
|
useKeyBinding(['i'], goToImport);
|
|
110
133
|
useKeyBinding(KEYBINDINGS.DETACH, handleDetach);
|
|
134
|
+
useKeyBinding(KEYBINDINGS.RESUME_BACKGROUND, handleResumeBackground);
|
|
111
135
|
// Memoize the view component to prevent unnecessary remounts
|
|
112
136
|
// Only recreate when currentView actually changes
|
|
113
137
|
const currentView = useMemo(() => {
|
package/dist/source/main.js
CHANGED
|
@@ -15,7 +15,6 @@ import { usePlayer } from "./hooks/usePlayer.js";
|
|
|
15
15
|
import { useYouTubeMusic } from "./hooks/useYouTubeMusic.js";
|
|
16
16
|
import { VIEW } from "./utils/constants.js";
|
|
17
17
|
import { getConfigService } from "./services/config/config.service.js";
|
|
18
|
-
import { getPlayerService } from "./services/player/player.service.js";
|
|
19
18
|
import { getNotificationService } from "./services/notification/notification.service.js";
|
|
20
19
|
function Initializer({ flags }) {
|
|
21
20
|
const { dispatch } = useNavigation();
|
|
@@ -24,20 +23,12 @@ function Initializer({ flags }) {
|
|
|
24
23
|
useEffect(() => {
|
|
25
24
|
// Check for background playback state on startup
|
|
26
25
|
const config = getConfigService();
|
|
27
|
-
const player = getPlayerService();
|
|
28
26
|
const backgroundState = config.getBackgroundPlaybackState();
|
|
29
27
|
if (backgroundState.enabled) {
|
|
30
28
|
// Show notification about background playback
|
|
31
29
|
const notification = getNotificationService();
|
|
32
30
|
notification.setEnabled(true);
|
|
33
|
-
void notification.notify('Background Playback Active', 'Press R to resume control');
|
|
34
|
-
// Try to reattach to the existing mpv process
|
|
35
|
-
if (backgroundState.ipcPath) {
|
|
36
|
-
void player.reattach(backgroundState.ipcPath).catch(() => {
|
|
37
|
-
// Reattach failed, clear the state
|
|
38
|
-
config.clearBackgroundPlaybackState();
|
|
39
|
-
});
|
|
40
|
-
}
|
|
31
|
+
void notification.notify('Background Playback Active', 'Press Shift+R to resume control');
|
|
41
32
|
}
|
|
42
33
|
if (flags?.showSuggestions) {
|
|
43
34
|
dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
|
|
@@ -235,7 +235,11 @@ class PlayerService {
|
|
|
235
235
|
mpvArgs.push(playUrl);
|
|
236
236
|
// Capture process in local var so stale exit handlers from a killed
|
|
237
237
|
// process don't overwrite state belonging to a newly-spawned process.
|
|
238
|
-
const spawnedProcess = spawn(this.getMpvCommand(), mpvArgs
|
|
238
|
+
const spawnedProcess = spawn(this.getMpvCommand(), mpvArgs, {
|
|
239
|
+
detached: true,
|
|
240
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
241
|
+
windowsHide: true,
|
|
242
|
+
});
|
|
239
243
|
this.mpvProcess = spawnedProcess;
|
|
240
244
|
if (!spawnedProcess.stdout || !spawnedProcess.stderr) {
|
|
241
245
|
throw new Error('Failed to create mpv process streams');
|
|
@@ -373,6 +377,10 @@ class PlayerService {
|
|
|
373
377
|
ipcPath: this.ipcPath,
|
|
374
378
|
currentUrl: this.currentUrl,
|
|
375
379
|
};
|
|
380
|
+
if (this.mpvProcess) {
|
|
381
|
+
// Allow detached mpv process to survive after CLI exits.
|
|
382
|
+
this.mpvProcess.unref();
|
|
383
|
+
}
|
|
376
384
|
// Clear references but DON'T kill mpv process - it keeps playing
|
|
377
385
|
this.mpvProcess = null;
|
|
378
386
|
this.ipcSocket = null;
|
|
@@ -37,6 +37,7 @@ export declare const KEYBINDINGS: {
|
|
|
37
37
|
readonly SETTINGS: readonly [","];
|
|
38
38
|
readonly PLUGINS: readonly ["p"];
|
|
39
39
|
readonly DETACH: readonly ["shift+q"];
|
|
40
|
+
readonly RESUME_BACKGROUND: readonly ["shift+r"];
|
|
40
41
|
readonly PLAY_PAUSE: readonly [" "];
|
|
41
42
|
readonly NEXT: readonly ["n", "right"];
|
|
42
43
|
readonly PREVIOUS: readonly ["b", "left"];
|
|
Binary file
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -105,7 +105,7 @@ brew install involvex/youtube-music-cli/youtube-music-cli
|
|
|
105
105
|
winget install Involvex.YoutubeMusicCLI
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
> Maintainers: tag pushes trigger `.github/workflows/homebrew-publish.yml` and `.github/workflows/winget-publish.yml`. Homebrew uses the tap format `involvex/youtube-music-cli/youtube-music-cli`, so ensure the formula file exists on the default branch at `Formula/youtube-music-cli.rb` for the tap installation to work.
|
|
108
|
+
> Maintainers: tag pushes trigger `.github/workflows/homebrew-publish.yml` and `.github/workflows/winget-publish.yml`. Homebrew uses the tap format `involvex/youtube-music-cli/youtube-music-cli`, so ensure the formula file exists on the default branch at `Formula/youtube-music-cli.rb` for the tap installation to work. Winget needs `WINGETCREATE_TOKEN` (GitHub PAT with `public_repo`) and a one-time initial submission to winget-pkgs before automated updates can run.
|
|
109
109
|
|
|
110
110
|
### From Source
|
|
111
111
|
|