@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 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 (navState.currentView !== VIEW.PLUGINS) {
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 { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
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: ["Limit: ", navState.searchLimit, " (Use [ or ] to adjust)"] }), _jsx(SearchBar, { isActive: isTyping && !isSearching, onInput: input => {
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 { useCallback, useEffect } from 'react';
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 memoizedHandler = useCallback(handler, [handler]);
14
+ const handlerRef = useRef(handler);
15
+ handlerRef.current = handler;
14
16
  useEffect(() => {
15
- const entry = { keys, handler: memoizedHandler };
17
+ const entry = { keys, handler: () => handlerRef.current() };
16
18
  registry.add(entry);
17
19
  return () => {
18
20
  registry.delete(entry);
19
21
  };
20
- }, [keys, memoizedHandler]);
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
- if (!hasShift && key.shift)
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
- dispatch({ category: 'PLAY', track });
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.27",
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.7.0",
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.0.0",
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.18.0"
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.2.3",
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.5.13",
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.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",