@involvex/youtube-music-cli 0.0.27 → 0.0.29
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/source/components/layouts/MainLayout.js +3 -1
- package/dist/source/components/layouts/SearchLayout.js +5 -2
- package/dist/source/hooks/useKeyboard.d.ts +1 -0
- package/dist/source/hooks/useKeyboard.js +15 -5
- package/dist/source/hooks/usePlayer.js +19 -11
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.0.29](https://github.com/involvex/youtube-music-cli/compare/v0.0.28...v0.0.29) (2026-02-22)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- prevent import navigation when in settings view ([d805b5a](https://github.com/involvex/youtube-music-cli/commit/d805b5a182a2241ab5494565dcf6645da82da954))
|
|
6
|
+
|
|
7
|
+
## [0.0.28](https://github.com/involvex/youtube-music-cli/compare/v0.0.27...v0.0.28) (2026-02-22)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- standardize quote style in snyk_rules.instructions.md ([5906fed](https://github.com/involvex/youtube-music-cli/commit/5906fed587be5f9cd6629281428ef304d8b32749))
|
|
12
|
+
|
|
1
13
|
## [0.0.27](https://github.com/involvex/youtube-music-cli/compare/v0.0.26...v0.0.27) (2026-02-20)
|
|
2
14
|
|
|
3
15
|
## [0.0.26](https://github.com/involvex/youtube-music-cli/compare/v0.0.25...v0.0.26) (2026-02-20)
|
|
@@ -80,7 +80,9 @@ function MainLayout() {
|
|
|
80
80
|
}, [dispatch, navState.currentView]);
|
|
81
81
|
const goToImport = useCallback(() => {
|
|
82
82
|
// Don't navigate to import if we're in plugins view (i key is used for plugin install there)
|
|
83
|
-
if (
|
|
83
|
+
// Don't navigate to import if we're in settings view (user navigates settings items with Enter)
|
|
84
|
+
if (navState.currentView !== VIEW.PLUGINS &&
|
|
85
|
+
navState.currentView !== VIEW.SETTINGS) {
|
|
84
86
|
dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
|
|
85
87
|
}
|
|
86
88
|
}, [dispatch, navState.currentView]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Search view layout
|
|
3
3
|
import { useNavigation } from "../../hooks/useNavigation.js";
|
|
4
4
|
import { useYouTubeMusic } from "../../hooks/useYouTubeMusic.js";
|
|
@@ -10,9 +10,11 @@ import SearchBar from "../search/SearchBar.js";
|
|
|
10
10
|
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
11
11
|
import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
|
+
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
13
14
|
function SearchLayout() {
|
|
14
15
|
const { theme } = useTheme();
|
|
15
16
|
const { state: navState, dispatch } = useNavigation();
|
|
17
|
+
const { state: playerState } = usePlayer();
|
|
16
18
|
const { isLoading, error, search } = useYouTubeMusic();
|
|
17
19
|
const [results, setResults] = useState([]);
|
|
18
20
|
const [isTyping, setIsTyping] = useState(true);
|
|
@@ -116,7 +118,8 @@ function SearchLayout() {
|
|
|
116
118
|
lastAutoSearchedQueryRef.current = null;
|
|
117
119
|
};
|
|
118
120
|
}, [dispatch]);
|
|
119
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.dim, children:
|
|
121
|
+
return (_jsxs(Box, { flexDirection: "column", children: [playerState.currentTrack && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.dim, children: playerState.isPlaying ? '▶ ' : '⏸ ' }), _jsx(Text, { color: theme.colors.primary, bold: true, children: playerState.currentTrack.title }), playerState.currentTrack.artists &&
|
|
122
|
+
playerState.currentTrack.artists.length > 0 && (_jsxs(Text, { color: theme.colors.secondary, children: [' • ', playerState.currentTrack.artists.map(a => a.name).join(', ')] }))] })), _jsxs(Text, { color: theme.colors.dim, children: ["Limit: ", navState.searchLimit, " (Use [ or ] to adjust)"] }), _jsx(SearchBar, { isActive: isTyping && !isSearching, onInput: input => {
|
|
120
123
|
void performSearch(input);
|
|
121
124
|
} }), (isLoading || isSearching) && (_jsx(Text, { color: theme.colors.accent, children: "Searching..." })), error && _jsx(Text, { color: theme.colors.error, children: error }), !isLoading && navState.hasSearched && (_jsx(SearchResults, { results: results, selectedIndex: navState.selectedResult, isActive: !isTyping, onMixCreated: handleMixCreated, onDownloadStatus: handleDownloadStatus })), !isLoading && navState.hasSearched && results.length === 0 && !error && (_jsx(Text, { color: theme.colors.dim, children: "No results found" })), actionMessage && (_jsx(Text, { color: theme.colors.accent, children: actionMessage })), _jsx(Text, { color: theme.colors.dim, children: isTyping
|
|
122
125
|
? 'Type to search, Enter to start, Esc to clear'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook to bind keyboard shortcuts.
|
|
3
3
|
* This uses a centralized manager to avoid multiple useInput calls and memory leaks.
|
|
4
|
+
* Uses a ref-based approach to always call the latest handler without stale closures.
|
|
4
5
|
*/
|
|
5
6
|
export declare function useKeyBinding(keys: readonly string[], handler: () => void): void;
|
|
6
7
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Keyboard input handling hook
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { useInput } from 'ink';
|
|
4
4
|
import { logger } from "../services/logger/logger.service.js";
|
|
5
5
|
import { useKeyboardBlockContext } from "./useKeyboardBlocker.js";
|
|
@@ -8,16 +8,18 @@ const registry = new Set();
|
|
|
8
8
|
/**
|
|
9
9
|
* Hook to bind keyboard shortcuts.
|
|
10
10
|
* This uses a centralized manager to avoid multiple useInput calls and memory leaks.
|
|
11
|
+
* Uses a ref-based approach to always call the latest handler without stale closures.
|
|
11
12
|
*/
|
|
12
13
|
export function useKeyBinding(keys, handler) {
|
|
13
|
-
const
|
|
14
|
+
const handlerRef = useRef(handler);
|
|
15
|
+
handlerRef.current = handler;
|
|
14
16
|
useEffect(() => {
|
|
15
|
-
const entry = { keys, handler:
|
|
17
|
+
const entry = { keys, handler: () => handlerRef.current() };
|
|
16
18
|
registry.add(entry);
|
|
17
19
|
return () => {
|
|
18
20
|
registry.delete(entry);
|
|
19
21
|
};
|
|
20
|
-
}, [keys
|
|
22
|
+
}, [keys]); // keys is the only dep; handlerRef is a stable ref
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Global Keyboard Manager Component
|
|
@@ -84,7 +86,15 @@ export function KeyboardManager() {
|
|
|
84
86
|
return false;
|
|
85
87
|
if (hasShift && !key.shift && !uppercaseShiftInput)
|
|
86
88
|
return false;
|
|
87
|
-
|
|
89
|
+
// Block lowercase-only bindings when shift is active or the input is
|
|
90
|
+
// an uppercase letter (which implies Shift was held).
|
|
91
|
+
// Example: the 'p' (Plugins) binding must not fire when the user
|
|
92
|
+
// presses Shift+P, which should only trigger 'shift+p' (Playlists).
|
|
93
|
+
// Note: `input !== input.toLowerCase()` is true only for uppercase
|
|
94
|
+
// alphabetical characters, avoiding false positives on symbols/digits.
|
|
95
|
+
if (!hasShift &&
|
|
96
|
+
(key.shift ||
|
|
97
|
+
(input.length === 1 && input !== input.toLowerCase())))
|
|
88
98
|
return false;
|
|
89
99
|
// Check the actual key
|
|
90
100
|
if (mainKey === 'up' && key.upArrow)
|
|
@@ -5,22 +5,30 @@ import { getConfigService } from "../services/config/config.service.js";
|
|
|
5
5
|
export function usePlayer() {
|
|
6
6
|
const { state, dispatch, ...playerStore } = usePlayerStore();
|
|
7
7
|
const play = useCallback((track, options) => {
|
|
8
|
-
// Clear queue if requested (e.g., playing from search results)
|
|
9
8
|
if (options?.clearQueue) {
|
|
9
|
+
// When clearing the queue, always dispatch fresh play commands rather
|
|
10
|
+
// than relying on the stale queue state captured in this closure.
|
|
11
|
+
// This fixes a bug where a track already in the queue wouldn't replay
|
|
12
|
+
// after clearQueue because SET_QUEUE_POSITION would be dispatched
|
|
13
|
+
// against the (now-empty) queue.
|
|
10
14
|
dispatch({ category: 'CLEAR_QUEUE' });
|
|
11
|
-
}
|
|
12
|
-
// Add to queue if not already there
|
|
13
|
-
const isInQueue = state.queue.some(t => t.videoId === track.videoId);
|
|
14
|
-
if (!isInQueue) {
|
|
15
15
|
dispatch({ category: 'ADD_TO_QUEUE', track });
|
|
16
|
-
|
|
17
|
-
// Find position and play
|
|
18
|
-
const position = state.queue.findIndex(t => t.videoId === track.videoId);
|
|
19
|
-
if (position >= 0) {
|
|
20
|
-
dispatch({ category: 'SET_QUEUE_POSITION', position });
|
|
16
|
+
dispatch({ category: 'PLAY', track });
|
|
21
17
|
}
|
|
22
18
|
else {
|
|
23
|
-
|
|
19
|
+
// Add to queue if not already there
|
|
20
|
+
const isInQueue = state.queue.some(t => t.videoId === track.videoId);
|
|
21
|
+
if (!isInQueue) {
|
|
22
|
+
dispatch({ category: 'ADD_TO_QUEUE', track });
|
|
23
|
+
}
|
|
24
|
+
// Find position and play
|
|
25
|
+
const position = state.queue.findIndex(t => t.videoId === track.videoId);
|
|
26
|
+
if (position >= 0) {
|
|
27
|
+
dispatch({ category: 'SET_QUEUE_POSITION', position });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
dispatch({ category: 'PLAY', track });
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
// Add to history
|
|
26
34
|
const config = getConfigService();
|
|
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.29",
|
|
4
4
|
"description": "- A Commandline music player for youtube-music",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -65,11 +65,11 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@distube/ytdl-core": "^4.16.12",
|
|
67
67
|
"ansi-escapes": "^7.3.0",
|
|
68
|
-
"ink": "^6.
|
|
68
|
+
"ink": "^6.8.0",
|
|
69
69
|
"ink-table": "^3.1.0",
|
|
70
70
|
"ink-text-input": "^6.0.0",
|
|
71
71
|
"jiti": "^2.6.1",
|
|
72
|
-
"meow": "^14.
|
|
72
|
+
"meow": "^14.1.0",
|
|
73
73
|
"node-notifier": "^10.0.1",
|
|
74
74
|
"discord-rpc": "^4.0.1",
|
|
75
75
|
"node-youtube-music": "^0.10.3",
|
|
@@ -77,20 +77,20 @@
|
|
|
77
77
|
"react": "^19.2.4",
|
|
78
78
|
"youtube-ext": "^1.1.25",
|
|
79
79
|
"youtubei.js": "^16.0.1",
|
|
80
|
-
"ws": "^8.
|
|
80
|
+
"ws": "^8.19.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@eslint/js": "^10.0.1",
|
|
84
84
|
"@sindresorhus/tsconfig": "^8.1.0",
|
|
85
|
-
"@types/node": "^25.
|
|
85
|
+
"@types/node": "^25.3.0",
|
|
86
86
|
"@types/node-notifier": "^8.0.5",
|
|
87
87
|
"@types/react": "^19.2.14",
|
|
88
|
-
"@types/ws": "^8.
|
|
88
|
+
"@types/ws": "^8.18.1",
|
|
89
89
|
"@vdemedes/prettier-config": "^2.0.1",
|
|
90
90
|
"ava": "^6.4.1",
|
|
91
91
|
"chalk": "^5.6.2",
|
|
92
92
|
"conventional-changelog-cli": "^5.0.0",
|
|
93
|
-
"eslint": "^10.0.
|
|
93
|
+
"eslint": "^10.0.1",
|
|
94
94
|
"eslint-plugin-react": "^7.37.5",
|
|
95
95
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
96
96
|
"globals": "^17.3.0",
|