@involvex/youtube-music-cli 0.0.15 → 0.0.17

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 CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.0.17](https://github.com/involvex/youtube-music-cli/compare/v0.0.16...v0.0.17) (2026-02-18)
2
+
3
+ ### Features
4
+
5
+ - **ui:** add playback mode and repeat indicators ([bc499a2](https://github.com/involvex/youtube-music-cli/commit/bc499a2083063d3bda3d4b1b1f1a85d620be9fd1))
6
+
7
+ ## [0.0.16](https://github.com/involvex/youtube-music-cli/compare/v0.0.15...v0.0.16) (2026-02-18)
8
+
9
+ ### Bug Fixes
10
+
11
+ - correct shuffle hotkey and mpv error handling ([7a08643](https://github.com/involvex/youtube-music-cli/commit/7a0864395b48c8bc1b30d817edc5b2f7cb9c79c6))
12
+
13
+ ### Features
14
+
15
+ - implement shuffle mode with Shift+S hotkey ([77be9ae](https://github.com/involvex/youtube-music-cli/commit/77be9ae53650dc3bf40a89427fbb1b6eef32683a))
16
+
1
17
  ## [0.0.15](https://github.com/involvex/youtube-music-cli/compare/v0.0.14...v0.0.15) (2026-02-18)
2
18
 
3
19
  ### Features
@@ -6,5 +6,5 @@ import { useNavigation } from "../../hooks/useNavigation.js";
6
6
  export default function Help() {
7
7
  const { theme } = useTheme();
8
8
  const { dispatch: _dispatch } = useNavigation();
9
- 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, { 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: "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" }), " or", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " to close"] })] })] }));
9
+ 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, { 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" }), " or", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " to close"] })] })] }));
10
10
  }
@@ -7,7 +7,7 @@ import { useKeyBinding } from "../../hooks/useKeyboard.js";
7
7
  import { KEYBINDINGS } from "../../utils/constants.js";
8
8
  export default function ShortcutsBar() {
9
9
  const { theme } = useTheme();
10
- const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, } = usePlayer();
10
+ const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, } = usePlayer();
11
11
  // Register key bindings globally
12
12
  const handlePlayPause = () => {
13
13
  if (playerState.isPlaying) {
@@ -24,6 +24,10 @@ export default function ShortcutsBar() {
24
24
  useKeyBinding(KEYBINDINGS.VOLUME_DOWN, volumeDown);
25
25
  useKeyBinding(KEYBINDINGS.VOLUME_FINE_UP, volumeFineUp);
26
26
  useKeyBinding(KEYBINDINGS.VOLUME_FINE_DOWN, volumeFineDown);
27
+ useKeyBinding(KEYBINDINGS.SHUFFLE, toggleShuffle);
28
+ useKeyBinding(KEYBINDINGS.REPEAT, toggleRepeat);
27
29
  // Note: SETTINGS keybinding handled by MainLayout to avoid double-dispatch
28
- return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Shortcuts: ", _jsx(Text, { color: theme.colors.text, children: "Space" }), " Play/Pause |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2192" }), " Next |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2190" }), " Prev |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+P" }), " Playlists |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+D" }), " Download |", ' ', _jsx(Text, { color: theme.colors.text, children: "m" }), " Mix |", ' ', _jsx(Text, { color: theme.colors.text, children: "M" }), " Mini |", ' ', _jsx(Text, { color: theme.colors.text, children: "/" }), " Search |", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " Help |", ' ', _jsx(Text, { color: theme.colors.text, children: "q" }), " Quit"] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.dim, children: "[=/" }), "-", _jsx(Text, { color: theme.colors.dim, children: "]" }), " Vol:", ' ', _jsxs(Text, { color: theme.colors.primary, children: [playerState.volume, "%"] })] })] }));
30
+ return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Shortcuts: ", _jsx(Text, { color: theme.colors.text, children: "Space" }), " Play/Pause |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2192" }), " Next |", ' ', _jsx(Text, { color: theme.colors.text, children: "\u2190" }), " Prev |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+S" }), " \uD83D\uDD00 |", ' ', _jsx(Text, { color: theme.colors.text, children: "r" }), " \uD83D\uDD04 |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+P" }), " Playlists |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+D" }), " Download |", ' ', _jsx(Text, { color: theme.colors.text, children: "m" }), " Mix |", ' ', _jsx(Text, { color: theme.colors.text, children: "/" }), " Search |", ' ', _jsx(Text, { color: theme.colors.text, children: "?" }), " Help |", ' ', _jsx(Text, { color: theme.colors.text, children: "q" }), " Quit"] }), _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
+ ? theme.colors.dim
32
+ : theme.colors.secondary, children: playerState.repeat === 'one' ? '🔂' : '🔄' }), ' ', _jsx(Text, { color: theme.colors.dim, children: "[=/" }), "-", _jsx(Text, { color: theme.colors.dim, children: "]" }), " Vol:", ' ', _jsxs(Text, { color: theme.colors.primary, children: [playerState.volume, "%"] })] })] }));
29
33
  }
@@ -40,5 +40,5 @@ export default function NowPlaying() {
40
40
  const percentage = duration > 0 ? Math.min(100, Math.floor((progress / duration) * 100)) : 0;
41
41
  const barWidth = Math.max(10, columns - 8);
42
42
  const filledWidth = duration > 0 ? Math.floor((progress / duration) * barWidth) : 0;
43
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.primary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: track.title }), _jsx(Text, { color: theme.colors.dim, children: " \u2022 " }), _jsx(Text, { color: theme.colors.secondary, children: artists })] }), track.album && _jsx(Text, { color: theme.colors.dim, children: track.album.name }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '█'.repeat(Math.min(filledWidth, barWidth)) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(Math.max(0, barWidth - filledWidth)) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsxs(Text, { color: theme.colors.dim, children: [" / ", formatTime(duration), " "] }), _jsxs(Text, { color: theme.colors.dim, children: ["[", percentage, "%]"] }), playerState.isLoading && (_jsx(Text, { color: theme.colors.accent, children: " Loading..." })), !playerState.isPlaying && progress > 0 && (_jsx(Text, { color: theme.colors.dim, children: " \u23F8" })), sleepRemaining !== null && (_jsxs(Text, { color: theme.colors.warning, children: [' ', "\u23FE ", formatTime(sleepRemaining)] }))] }), playerState.error && (_jsx(Text, { color: theme.colors.error, children: playerState.error }))] }));
43
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.primary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: track.title }), _jsx(Text, { color: theme.colors.dim, children: " \u2022 " }), _jsx(Text, { color: theme.colors.secondary, children: artists })] }), track.album && _jsx(Text, { color: theme.colors.dim, children: track.album.name }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '█'.repeat(Math.min(filledWidth, barWidth)) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(Math.max(0, barWidth - filledWidth)) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsxs(Text, { color: theme.colors.dim, children: [" / ", formatTime(duration), " "] }), _jsxs(Text, { color: theme.colors.dim, children: ["[", percentage, "%]"] }), playerState.isLoading && (_jsx(Text, { color: theme.colors.accent, children: " Loading..." })), !playerState.isPlaying && progress > 0 && (_jsx(Text, { color: theme.colors.dim, children: " \u23F8" })), playerState.shuffle && _jsx(Text, { color: theme.colors.primary, children: " \uD83D\uDD00" }), sleepRemaining !== null && (_jsxs(Text, { color: theme.colors.warning, children: [' ', "\u23FE ", formatTime(sleepRemaining)] }))] }), playerState.error && (_jsx(Text, { color: theme.colors.error, children: playerState.error }))] }));
44
44
  }
@@ -17,7 +17,7 @@ export default function PlayerControls() {
17
17
  };
18
18
  }, [instanceId]);
19
19
  const { theme } = useTheme();
20
- const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, speedUp, speedDown, } = usePlayer();
20
+ const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, speedUp, speedDown, toggleShuffle, } = usePlayer();
21
21
  // DEBUG: Log when callbacks change (detect instability)
22
22
  useEffect(() => {
23
23
  // Temporarily output to stderr to debug without triggering Ink re-render
@@ -39,5 +39,6 @@ export default function PlayerControls() {
39
39
  useKeyBinding(KEYBINDINGS.VOLUME_DOWN, volumeDown);
40
40
  useKeyBinding(KEYBINDINGS.SPEED_UP, speedUp);
41
41
  useKeyBinding(KEYBINDINGS.SPEED_DOWN, speedDown);
42
- return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 2, borderStyle: "classic", borderColor: theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2190 / b" }), "] Prev"] }), _jsx(Text, { color: theme.colors.primary, children: playerState.isPlaying ? (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Pause"] })) : (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Play"] })) }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2192 / n" }), "] Next"] }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "+/-" }), "] Vol: ", playerState.volume, "%"] }), (playerState.speed ?? 1.0) !== 1.0 && (_jsxs(Text, { color: theme.colors.accent, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "<>" }), "]", ' ', (playerState.speed ?? 1.0).toFixed(2), "x"] }))] }));
42
+ useKeyBinding(KEYBINDINGS.SHUFFLE, toggleShuffle);
43
+ return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 2, borderStyle: "classic", borderColor: theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2190 / b" }), "] Prev"] }), _jsx(Text, { color: theme.colors.primary, children: playerState.isPlaying ? (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Pause"] })) : (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Play"] })) }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2192 / n" }), "] Next"] }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "+/-" }), "] Vol: ", playerState.volume, "%"] }), _jsxs(Text, { color: playerState.shuffle ? theme.colors.primary : theme.colors.dim, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Shift+S" }), "]", ' ', playerState.shuffle ? '🔀 ON' : '🔀 OFF'] }), (playerState.speed ?? 1.0) !== 1.0 && (_jsxs(Text, { color: theme.colors.accent, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "<>" }), "]", ' ', (playerState.speed ?? 1.0).toFixed(2), "x"] }))] }));
43
44
  }
@@ -75,11 +75,14 @@ export function KeyboardManager() {
75
75
  const hasMeta = parts.includes('meta') || parts.includes('alt');
76
76
  const hasShift = parts.includes('shift');
77
77
  const mainKey = parts[parts.length - 1];
78
+ const uppercaseShiftInput = input.length === 1 &&
79
+ input === input.toUpperCase() &&
80
+ input.toLowerCase() === mainKey;
78
81
  if (hasCtrl && !key.ctrl)
79
82
  return false;
80
83
  if (hasMeta && !key.meta)
81
84
  return false;
82
- if (hasShift && !key.shift)
85
+ if (hasShift && !key.shift && !uppercaseShiftInput)
83
86
  return false;
84
87
  if (!hasShift && key.shift)
85
88
  return false;
@@ -33,6 +33,7 @@ declare class PlayerService {
33
33
  * Generate IPC socket path based on platform, unique per play session
34
34
  */
35
35
  private getIpcPath;
36
+ private getMpvCommand;
36
37
  /**
37
38
  * Connect to mpv IPC socket
38
39
  */
@@ -44,6 +44,13 @@ class PlayerService {
44
44
  return `/tmp/mpvsocket-${process.pid}-${this.playSessionId}`;
45
45
  }
46
46
  }
47
+ getMpvCommand() {
48
+ const configuredPath = process.env['MPV_PATH']?.trim();
49
+ if (configuredPath) {
50
+ return configuredPath;
51
+ }
52
+ return process.platform === 'win32' ? 'mpv.exe' : 'mpv';
53
+ }
47
54
  /**
48
55
  * Connect to mpv IPC socket
49
56
  */
@@ -228,7 +235,7 @@ class PlayerService {
228
235
  mpvArgs.push(playUrl);
229
236
  // Capture process in local var so stale exit handlers from a killed
230
237
  // process don't overwrite state belonging to a newly-spawned process.
231
- const spawnedProcess = spawn('mpv', mpvArgs);
238
+ const spawnedProcess = spawn(this.getMpvCommand(), mpvArgs);
232
239
  this.mpvProcess = spawnedProcess;
233
240
  if (!spawnedProcess.stdout || !spawnedProcess.stderr) {
234
241
  throw new Error('Failed to create mpv process streams');
@@ -288,6 +295,10 @@ class PlayerService {
288
295
  this.isPlaying = false;
289
296
  this.mpvProcess = null;
290
297
  }
298
+ if ('code' in error && error.code === 'ENOENT') {
299
+ reject(new Error("mpv executable not found. Install mpv and ensure it's in PATH (or set MPV_PATH)."));
300
+ return;
301
+ }
291
302
  reject(error);
292
303
  });
293
304
  logger.info('PlayerService', 'mpv process started successfully');
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { PlayerState, PlayerAction } from '../types/player.types.ts';
3
+ export declare function playerReducer(state: PlayerState, action: PlayerAction): PlayerState;
3
4
  import type { Track } from '../types/youtube-music.types.ts';
4
5
  type PlayerContextValue = {
5
6
  state: PlayerState;
@@ -24,7 +24,7 @@ const initialState = {
24
24
  };
25
25
  // Get player service instance
26
26
  const playerService = getPlayerService();
27
- function playerReducer(state, action) {
27
+ export function playerReducer(state, action) {
28
28
  switch (action.category) {
29
29
  case 'PLAY':
30
30
  return {
@@ -45,7 +45,23 @@ function playerReducer(state, action) {
45
45
  progress: 0,
46
46
  currentTrack: null,
47
47
  };
48
- case 'NEXT':
48
+ case 'NEXT': {
49
+ if (state.queue.length === 0)
50
+ return state;
51
+ // Shuffle mode: pick a random track excluding the current position
52
+ if (state.shuffle && state.queue.length > 1) {
53
+ let randomIndex;
54
+ do {
55
+ randomIndex = Math.floor(Math.random() * state.queue.length);
56
+ } while (randomIndex === state.queuePosition);
57
+ return {
58
+ ...state,
59
+ queuePosition: randomIndex,
60
+ currentTrack: state.queue[randomIndex] ?? null,
61
+ progress: 0,
62
+ };
63
+ }
64
+ // Sequential mode
49
65
  const nextPosition = state.queuePosition + 1;
50
66
  if (nextPosition >= state.queue.length) {
51
67
  if (state.repeat === 'all') {
@@ -64,6 +80,7 @@ function playerReducer(state, action) {
64
80
  currentTrack: state.queue[nextPosition] ?? null,
65
81
  progress: 0,
66
82
  };
83
+ }
67
84
  case 'PREVIOUS':
68
85
  const prevPosition = state.queuePosition - 1;
69
86
  if (prevPosition < 0) {
@@ -41,7 +41,7 @@ export declare const KEYBINDINGS: {
41
41
  readonly VOLUME_DOWN: readonly ["-"];
42
42
  readonly VOLUME_FINE_UP: readonly ["shift+="];
43
43
  readonly VOLUME_FINE_DOWN: readonly ["shift+-"];
44
- readonly SHUFFLE: readonly ["s"];
44
+ readonly SHUFFLE: readonly ["shift+s"];
45
45
  readonly REPEAT: readonly ["r"];
46
46
  readonly SEEK_FORWARD: readonly ["shift+right"];
47
47
  readonly SEEK_BACKWARD: readonly ["shift+left"];
@@ -50,7 +50,7 @@ export const KEYBINDINGS = {
50
50
  VOLUME_DOWN: ['-'], // Only '-' without shift
51
51
  VOLUME_FINE_UP: ['shift+='], // Fine-grained +1 step
52
52
  VOLUME_FINE_DOWN: ['shift+-'], // Fine-grained -1 step
53
- SHUFFLE: ['s'],
53
+ SHUFFLE: ['shift+s'],
54
54
  REPEAT: ['r'],
55
55
  SEEK_FORWARD: ['shift+right'],
56
56
  SEEK_BACKWARD: ['shift+left'],
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -78,6 +78,7 @@
78
78
  "devDependencies": {
79
79
  "@eslint/js": "^10.0.1",
80
80
  "@sindresorhus/tsconfig": "^8.1.0",
81
+ "@types/node": "^25.2.3",
81
82
  "@types/node-notifier": "^8.0.5",
82
83
  "@types/react": "^19.2.14",
83
84
  "@vdemedes/prettier-config": "^2.0.1",
package/readme.md CHANGED
@@ -377,4 +377,4 @@ Made with ❤️ for music lovers
377
377
 
378
378
  **[🪙 Paypal](https://paypal.me/involvex)**
379
379
 
380
- **⌨️ [Github Sponsors](https://github.com/sponsors/involvex)**
380
+ **[⌨️ Github Sponsors](https://github.com/sponsors/involvex)**