@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 +16 -0
- package/dist/source/components/common/Help.js +1 -1
- package/dist/source/components/common/ShortcutsBar.js +6 -2
- package/dist/source/components/player/NowPlaying.js +1 -1
- package/dist/source/components/player/PlayerControls.js +3 -2
- package/dist/source/hooks/useKeyboard.js +4 -1
- package/dist/source/services/player/player.service.d.ts +1 -0
- package/dist/source/services/player/player.service.js +12 -1
- package/dist/source/stores/player.store.d.ts +1 -0
- package/dist/source/stores/player.store.js +19 -2
- package/dist/source/utils/constants.d.ts +1 -1
- package/dist/source/utils/constants.js +1 -1
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +2 -1
- package/readme.md +1 -1
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: "
|
|
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+
|
|
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
|
-
|
|
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;
|
|
@@ -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(
|
|
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.
|
|
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