@involvex/youtube-music-cli 0.0.37 → 0.0.39
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 +11 -1
- package/dist/source/components/common/ShortcutsBar.js +57 -11
- package/dist/source/components/layouts/MainLayout.js +1 -1
- package/dist/source/services/player/player.service.js +1 -0
- package/dist/source/services/web/web-server-manager.js +1 -0
- package/dist/source/stores/player.store.js +22 -3
- package/dist/source/types/player.types.d.ts +1 -0
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
## [0.0.
|
|
1
|
+
## [0.0.39](https://github.com/involvex/youtube-music-cli/compare/v0.0.38...v0.0.39) (2026-02-23)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add visual flash feedback when shortcuts are pressed ([dc3efa8](https://github.com/involvex/youtube-music-cli/commit/dc3efa8f642619ad2e9ce937528e76f4d388e4dc))
|
|
6
|
+
|
|
7
|
+
## [0.0.38](https://github.com/involvex/youtube-music-cli/compare/v0.0.36...v0.0.38) (2026-02-22)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **search:** prevent 'q' key from triggering quit when typing in search bar ([32cd888](https://github.com/involvex/youtube-music-cli/commit/32cd888afcabc30d10988491da12c653cb5175d5))
|
|
2
12
|
|
|
3
13
|
## [0.0.36](https://github.com/involvex/youtube-music-cli/compare/v0.0.35...v0.0.36) (2026-02-22)
|
|
4
14
|
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// Shortcuts bar component
|
|
3
|
+
import { useState } from 'react';
|
|
3
4
|
import { Box, Text } from 'ink';
|
|
4
5
|
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
5
6
|
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
7
|
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
7
8
|
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
8
9
|
import { ICONS } from "../../utils/icons.js";
|
|
10
|
+
const FLASH_DURATION_MS = 300;
|
|
9
11
|
export default function ShortcutsBar() {
|
|
10
12
|
const { theme } = useTheme();
|
|
11
13
|
const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, } = usePlayer();
|
|
14
|
+
const [flashState, setFlashState] = useState({});
|
|
15
|
+
const flash = (key) => {
|
|
16
|
+
setFlashState(prev => ({ ...prev, [key]: true }));
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
setFlashState(prev => ({ ...prev, [key]: false }));
|
|
19
|
+
}, FLASH_DURATION_MS);
|
|
20
|
+
};
|
|
21
|
+
const shortcutColor = (key) => flashState[key] ? theme.colors.success : theme.colors.text;
|
|
12
22
|
// Register key bindings globally
|
|
13
23
|
const handlePlayPause = () => {
|
|
24
|
+
flash('playPause');
|
|
14
25
|
if (playerState.isPlaying) {
|
|
15
26
|
pause();
|
|
16
27
|
}
|
|
@@ -19,16 +30,51 @@ export default function ShortcutsBar() {
|
|
|
19
30
|
}
|
|
20
31
|
};
|
|
21
32
|
useKeyBinding(KEYBINDINGS.PLAY_PAUSE, handlePlayPause);
|
|
22
|
-
useKeyBinding(KEYBINDINGS.NEXT,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
useKeyBinding(KEYBINDINGS.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
useKeyBinding(KEYBINDINGS.NEXT, () => {
|
|
34
|
+
flash('next');
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
useKeyBinding(KEYBINDINGS.PREVIOUS, () => {
|
|
38
|
+
flash('prev');
|
|
39
|
+
previous();
|
|
40
|
+
});
|
|
41
|
+
useKeyBinding(KEYBINDINGS.VOLUME_UP, () => {
|
|
42
|
+
flash('volume');
|
|
43
|
+
volumeUp();
|
|
44
|
+
});
|
|
45
|
+
useKeyBinding(KEYBINDINGS.VOLUME_DOWN, () => {
|
|
46
|
+
flash('volume');
|
|
47
|
+
volumeDown();
|
|
48
|
+
});
|
|
49
|
+
useKeyBinding(KEYBINDINGS.VOLUME_FINE_UP, () => {
|
|
50
|
+
flash('volume');
|
|
51
|
+
volumeFineUp();
|
|
52
|
+
});
|
|
53
|
+
useKeyBinding(KEYBINDINGS.VOLUME_FINE_DOWN, () => {
|
|
54
|
+
flash('volume');
|
|
55
|
+
volumeFineDown();
|
|
56
|
+
});
|
|
57
|
+
useKeyBinding(KEYBINDINGS.SHUFFLE, () => {
|
|
58
|
+
flash('shuffle');
|
|
59
|
+
toggleShuffle();
|
|
60
|
+
});
|
|
61
|
+
useKeyBinding(KEYBINDINGS.REPEAT, () => {
|
|
62
|
+
flash('repeat');
|
|
63
|
+
toggleRepeat();
|
|
64
|
+
});
|
|
30
65
|
// Note: SETTINGS keybinding handled by MainLayout to avoid double-dispatch
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
const shuffleColor = flashState['shuffle']
|
|
67
|
+
? theme.colors.success
|
|
68
|
+
: playerState.shuffle
|
|
69
|
+
? theme.colors.primary
|
|
70
|
+
: theme.colors.dim;
|
|
71
|
+
const repeatColor = flashState['repeat']
|
|
72
|
+
? theme.colors.success
|
|
73
|
+
: playerState.repeat !== 'off'
|
|
74
|
+
? theme.colors.secondary
|
|
75
|
+
: theme.colors.dim;
|
|
76
|
+
const volumeColor = flashState['volume']
|
|
77
|
+
? theme.colors.success
|
|
78
|
+
: theme.colors.primary;
|
|
79
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsxs(Text, { color: shortcutColor('playPause'), children: [playerState.isPlaying ? ICONS.PAUSE : ICONS.PLAY_PAUSE_ON, " [Space]"] }), ' ', "| ", _jsxs(Text, { color: shortcutColor('prev'), children: [ICONS.PREV, " [B/\u2190]"] }), " |", ' ', _jsxs(Text, { color: shortcutColor('next'), children: [ICONS.NEXT, " [N/\u2192]"] }), " |", ' ', _jsxs(Text, { color: shuffleColor, children: [ICONS.SHUFFLE, " [Shift+S]"] }), " |", ' ', _jsxs(Text, { color: repeatColor, children: [playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL, ' ', "[R]"] }), ' ', "| ", _jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAYLIST, " [Shift+P]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.DOWNLOAD, " [Shift+D]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SEARCH, " [/]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.HELP, " [?]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.BG_PLAY, " [Shift+Q]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.RESUME, " [Shift+R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.QUIT, " [Q]"] })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: shuffleColor, children: ICONS.SHUFFLE }), ' ', _jsx(Text, { color: repeatColor, children: playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL }), ' ', _jsxs(Text, { color: theme.colors.dim, children: [ICONS.VOLUME, " [+/-]"] }), ' ', _jsxs(Text, { color: volumeColor, children: [playerState.volume, "%"] })] })] }));
|
|
34
80
|
}
|
|
@@ -124,7 +124,7 @@ function MainLayout() {
|
|
|
124
124
|
dispatch({ category: 'TOGGLE_PLAYER_MODE' });
|
|
125
125
|
}, [dispatch]);
|
|
126
126
|
// Global keyboard bindings
|
|
127
|
-
useKeyBinding(KEYBINDINGS.QUIT, handleQuit
|
|
127
|
+
useKeyBinding(KEYBINDINGS.QUIT, handleQuit);
|
|
128
128
|
useKeyBinding(KEYBINDINGS.SEARCH, goToSearch);
|
|
129
129
|
useKeyBinding(KEYBINDINGS.PLAYLISTS, goToPlaylists);
|
|
130
130
|
useKeyBinding(KEYBINDINGS.PLUGINS, goToPlugins);
|
|
@@ -23,6 +23,7 @@ const initialState = {
|
|
|
23
23
|
shuffle: false,
|
|
24
24
|
isLoading: false,
|
|
25
25
|
error: null,
|
|
26
|
+
playRequestId: 0,
|
|
26
27
|
};
|
|
27
28
|
// Get player service instance
|
|
28
29
|
const playerService = getPlayerService();
|
|
@@ -35,6 +36,7 @@ export function playerReducer(state, action) {
|
|
|
35
36
|
isPlaying: true,
|
|
36
37
|
progress: 0,
|
|
37
38
|
error: null,
|
|
39
|
+
playRequestId: state.playRequestId + 1,
|
|
38
40
|
};
|
|
39
41
|
case 'PAUSE':
|
|
40
42
|
return { ...state, isPlaying: false };
|
|
@@ -62,6 +64,7 @@ export function playerReducer(state, action) {
|
|
|
62
64
|
currentTrack: state.queue[randomIndex] ?? null,
|
|
63
65
|
isPlaying: true,
|
|
64
66
|
progress: 0,
|
|
67
|
+
playRequestId: state.playRequestId + 1,
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
// Sequential mode
|
|
@@ -74,6 +77,7 @@ export function playerReducer(state, action) {
|
|
|
74
77
|
currentTrack: state.queue[0] ?? null,
|
|
75
78
|
isPlaying: true,
|
|
76
79
|
progress: 0,
|
|
80
|
+
playRequestId: state.playRequestId + 1,
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
return state;
|
|
@@ -84,6 +88,7 @@ export function playerReducer(state, action) {
|
|
|
84
88
|
currentTrack: state.queue[nextPosition] ?? null,
|
|
85
89
|
isPlaying: true,
|
|
86
90
|
progress: 0,
|
|
91
|
+
playRequestId: state.playRequestId + 1,
|
|
87
92
|
};
|
|
88
93
|
}
|
|
89
94
|
case 'PREVIOUS':
|
|
@@ -95,6 +100,7 @@ export function playerReducer(state, action) {
|
|
|
95
100
|
return {
|
|
96
101
|
...state,
|
|
97
102
|
progress: 0,
|
|
103
|
+
playRequestId: state.playRequestId + 1,
|
|
98
104
|
};
|
|
99
105
|
}
|
|
100
106
|
return {
|
|
@@ -102,6 +108,7 @@ export function playerReducer(state, action) {
|
|
|
102
108
|
queuePosition: prevPosition,
|
|
103
109
|
currentTrack: state.queue[prevPosition] ?? null,
|
|
104
110
|
progress: 0,
|
|
111
|
+
playRequestId: state.playRequestId + 1,
|
|
105
112
|
};
|
|
106
113
|
case 'SEEK':
|
|
107
114
|
return {
|
|
@@ -174,6 +181,7 @@ export function playerReducer(state, action) {
|
|
|
174
181
|
queuePosition: action.position,
|
|
175
182
|
currentTrack: state.queue[action.position] ?? null,
|
|
176
183
|
progress: 0,
|
|
184
|
+
playRequestId: state.playRequestId + 1,
|
|
177
185
|
};
|
|
178
186
|
}
|
|
179
187
|
return state;
|
|
@@ -207,6 +215,7 @@ export function playerReducer(state, action) {
|
|
|
207
215
|
hasTrack: !!action.currentTrack,
|
|
208
216
|
queueLength: action.queue.length,
|
|
209
217
|
});
|
|
218
|
+
playerService.setVolume(action.volume);
|
|
210
219
|
return {
|
|
211
220
|
...state,
|
|
212
221
|
currentTrack: action.currentTrack,
|
|
@@ -292,6 +301,7 @@ function PlayerManager() {
|
|
|
292
301
|
};
|
|
293
302
|
}, [dispatch, playerService]);
|
|
294
303
|
// Handle track changes
|
|
304
|
+
const lastPlayedRequestId = useRef(-1);
|
|
295
305
|
useEffect(() => {
|
|
296
306
|
const track = state.currentTrack;
|
|
297
307
|
if (!track) {
|
|
@@ -306,14 +316,17 @@ function PlayerManager() {
|
|
|
306
316
|
});
|
|
307
317
|
return;
|
|
308
318
|
}
|
|
309
|
-
// Guard:
|
|
319
|
+
// Guard: Don't replay same track unless a new play request was explicitly dispatched
|
|
310
320
|
const currentTrackId = playerService.getCurrentTrackId?.() || '';
|
|
311
|
-
|
|
321
|
+
const isSameTrack = currentTrackId === track.videoId;
|
|
322
|
+
const isNewPlayRequest = state.playRequestId !== lastPlayedRequestId.current;
|
|
323
|
+
if (isSameTrack && !isNewPlayRequest) {
|
|
312
324
|
logger.debug('PlayerManager', 'Track already playing, skipping', {
|
|
313
325
|
videoId: track.videoId,
|
|
314
326
|
});
|
|
315
327
|
return;
|
|
316
328
|
}
|
|
329
|
+
lastPlayedRequestId.current = state.playRequestId;
|
|
317
330
|
logger.info('PlayerManager', 'Loading track', {
|
|
318
331
|
title: track.title,
|
|
319
332
|
videoId: track.videoId,
|
|
@@ -424,7 +437,13 @@ function PlayerManager() {
|
|
|
424
437
|
void loadAndPlayTrack();
|
|
425
438
|
// Note: state.volume intentionally excluded - volume changes should not restart playback
|
|
426
439
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
427
|
-
}, [
|
|
440
|
+
}, [
|
|
441
|
+
state.currentTrack,
|
|
442
|
+
state.isPlaying,
|
|
443
|
+
state.playRequestId,
|
|
444
|
+
dispatch,
|
|
445
|
+
musicService,
|
|
446
|
+
]);
|
|
428
447
|
// Handle progress tracking
|
|
429
448
|
useEffect(() => {
|
|
430
449
|
if (state.isPlaying && state.currentTrack) {
|
|
@@ -13,5 +13,6 @@ export interface PlayerState {
|
|
|
13
13
|
shuffle: boolean;
|
|
14
14
|
isLoading: boolean;
|
|
15
15
|
error: string | null;
|
|
16
|
+
playRequestId: number;
|
|
16
17
|
}
|
|
17
18
|
export type PlayerAction = PlayAction | PauseAction | ResumeAction | StopAction | NextAction | PreviousAction | SeekAction | SetVolumeAction | VolumeUpAction | VolumeDownAction | VolumeFineUpAction | VolumeFineDownAction | ToggleShuffleAction | ToggleRepeatAction | SetQueueAction | AddToQueueAction | RemoveFromQueueAction | ClearQueueAction | SetQueuePositionAction | UpdateProgressAction | SetDurationAction | TickAction | SetLoadingAction | SetErrorAction | RestoreStateAction | SetSpeedAction;
|
|
Binary file
|