@involvex/youtube-music-cli 0.0.5 → 0.0.8

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.
@@ -0,0 +1,9 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [involvex]
4
+ custom:
5
+ [
6
+ 'https://buymeacoffee.com/involvex',
7
+ 'https://paypal.me/involvex',
8
+ 'https://rewards.bing.com/welcome?rh=14525F68&ref=rafsrchae&form=ML2XE3&OCID=ML2XE3&PUBL=RewardsDO&CREA=ML2XE3',
9
+ ]
package/CHANGELOG.md ADDED
@@ -0,0 +1,86 @@
1
+ ## [0.0.8](https://github.com/involvex/youtube-music-cli/compare/v0.0.7...v0.0.8) (2026-02-18)
2
+
3
+ ### Features
4
+
5
+ - **ui:** add keyboard blocker to search bar ([ee533fe](https://github.com/involvex/youtube-music-cli/commit/ee533fe716a203d432170e36149389f5bd48d687))
6
+
7
+ ## [0.0.7](https://github.com/involvex/youtube-music-cli/compare/v0.0.6...v0.0.7) (2026-02-18)
8
+
9
+ ### Features
10
+
11
+ - **ui:** add plugins and enhance playlist management ([83a043c](https://github.com/involvex/youtube-music-cli/commit/83a043c0956da7023ea198468db8bbc2ce3acb0d))
12
+
13
+ ### BREAKING CHANGES
14
+
15
+ - **ui:** The keybinding for playlists has changed from 'p' to
16
+ 'shift+p' to accommodate the new plugins feature.
17
+
18
+ ## [0.0.6](https://github.com/involvex/youtube-music-cli/compare/v0.0.5...v0.0.6) (2026-02-18)
19
+
20
+ ### Features
21
+
22
+ - **ui:** add playlist creation and artist playback features ([0f50fd2](https://github.com/involvex/youtube-music-cli/commit/0f50fd2eeda0d340aee26b8fa2f7e5f2356d8042))
23
+
24
+ ## [0.0.5](https://github.com/involvex/youtube-music-cli/compare/v0.0.4...v0.0.5) (2026-02-18)
25
+
26
+ ## [0.0.4](https://github.com/involvex/youtube-music-cli/compare/v0.0.3...v0.0.4) (2026-02-18)
27
+
28
+ ### Bug Fixes
29
+
30
+ - resolve three bugs - discord rpc, search history key, resume ([90c5306](https://github.com/involvex/youtube-music-cli/commit/90c530698f08c355e11d31048844b2e1d6a312ef))
31
+ - **youtube:** guard suggestions parsing errors ([aca832e](https://github.com/involvex/youtube-music-cli/commit/aca832e27bb244f8b42b33060f2640f3cc003f7f))
32
+
33
+ ### Features
34
+
35
+ - **assets:** add new icons and images ([a0d6558](https://github.com/involvex/youtube-music-cli/commit/a0d6558ed2fd42b470e7123eaac5ce0c602db051))
36
+
37
+ ## [0.0.3](https://github.com/involvex/youtube-music-cli/compare/v0.0.2...v0.0.3) (2026-02-18)
38
+
39
+ ### Features
40
+
41
+ - add playback speed control, new themes, and notifications ([426360a](https://github.com/involvex/youtube-music-cli/commit/426360adfde0a19d7cf9706371f5bc15a4e7640b))
42
+
43
+ ## [0.0.2](https://github.com/involvex/youtube-music-cli/compare/v0.0.1...v0.0.2) (2026-02-18)
44
+
45
+ ## [0.0.1](https://github.com/involvex/youtube-music-cli/compare/32798e7dd129656b9786fc435466203a0c913705...v0.0.1) (2026-02-18)
46
+
47
+ ### Bug Fixes
48
+
49
+ - **api:** resolve 404 error during search and improve reliability ([c26f80f](https://github.com/involvex/youtube-music-cli/commit/c26f80f3c7f4de075393da7cc378aada8809604a))
50
+ - **api:** resolve search runtime error and integrate real streaming ([3c8066c](https://github.com/involvex/youtube-music-cli/commit/3c8066c37386ed6b2d50249cbeec4b30e012960d))
51
+ - clear queue when playing from search results to match indices ([272690d](https://github.com/involvex/youtube-music-cli/commit/272690d2f1dd0f81fecfdad4e916f4a6f42392a2))
52
+ - **cli:** resolve Ink crash, prevent double instances, and add features ([2dfa274](https://github.com/involvex/youtube-music-cli/commit/2dfa2747a1729037e3f88656579042892aec0b26))
53
+ - **cli:** resolve search input issues, terminal auto-scrolling, and UI duplication ([f3898ad](https://github.com/involvex/youtube-music-cli/commit/f3898adde09d244e6705d970ab2c3af75cd7aec7))
54
+ - **cli:** resolve search trigger and improve UI stability ([ccce4b1](https://github.com/involvex/youtube-music-cli/commit/ccce4b1a354432f04e771a7ddd957bd7ec5b8e6d))
55
+ - **cli:** resolve terminal flooding, fix search selection, and modernize React imports ([1bdfae3](https://github.com/involvex/youtube-music-cli/commit/1bdfae307ee45cafe722590ed64c5c1fd22322ae))
56
+ - **hooks:** resolve memory leak in useKeyboard hook ([94f4d39](https://github.com/involvex/youtube-music-cli/commit/94f4d3974a4ad0a5e609f5cc3003933978fd9008))
57
+ - **lint:** disable no-explicit-any for react-hooks plugin in eslint.config.ts ([7a54c2a](https://github.com/involvex/youtube-music-cli/commit/7a54c2ab04dd6dfbfcc80a3c8ec86fdcb66c05c8))
58
+ - **lint:** resolve react-hooks/exhaustive-deps error and fix hook bugs ([19cf971](https://github.com/involvex/youtube-music-cli/commit/19cf9712b049e494fba1b86e5e08f37c5f24c91e))
59
+ - **security:** implement URL sanitization and resolve linting errors ([adb1d8f](https://github.com/involvex/youtube-music-cli/commit/adb1d8fa02830d470c14912a0c1155441224ec01))
60
+ - simplify VOLUME_UP and VOLUME_DOWN keybindings ([571b136](https://github.com/involvex/youtube-music-cli/commit/571b136bfefefc122e9da82ce4e615d3d576b9e3))
61
+ - **ui:** refine help display, fix help nav, and update key hints ([adb8afb](https://github.com/involvex/youtube-music-cli/commit/adb8afbba95c1495431502d9049e67bac10295a4))
62
+
63
+ ### Features
64
+
65
+ - add .gemini agent config for CLI UI design ([e27269e](https://github.com/involvex/youtube-music-cli/commit/e27269e88bd73a23bf668ef1374f4525d06a69bd))
66
+ - add @distube/ytdl-core dependency for YouTube downloading ([da5a8a7](https://github.com/involvex/youtube-music-cli/commit/da5a8a70cf4d65ff90dff3e7956c5d4e72740b7d))
67
+ - add compile script for building standalone executable ([ce3d731](https://github.com/involvex/youtube-music-cli/commit/ce3d7314e42f852b4c05eb8436463a1572ef6ed0))
68
+ - add config screen with keyboard navigation ([f9566cd](https://github.com/involvex/youtube-music-cli/commit/f9566cdd4f4818b158ecc5c45dae73b2af544f44))
69
+ - add Help component for keyboard shortcuts display ([32798e7](https://github.com/involvex/youtube-music-cli/commit/32798e7dd129656b9786fc435466203a0c913705))
70
+ - add player state persistence and npm publish workflow ([df7e5ce](https://github.com/involvex/youtube-music-cli/commit/df7e5ce10d13406ec0e284ff91ab8d1c38c23084))
71
+ - add plugin system API docs, templates, and context provider ([06392dc](https://github.com/involvex/youtube-music-cli/commit/06392dc95253fe90ddfc77d0bfbb0455b517fff6))
72
+ - add plugin system infrastructure and improve navigation ([626ada6](https://github.com/involvex/youtube-music-cli/commit/626ada679d530b66733ad58a553e72bd7b94755a))
73
+ - add react-devtools-core dependency and bun build script ([44a2a90](https://github.com/involvex/youtube-music-cli/commit/44a2a90a6c738d3bc4637bccf0d8513b2967c363))
74
+ - add ShortcutsBar component and prevent duplicate track playback ([af1fe32](https://github.com/involvex/youtube-music-cli/commit/af1fe32c42a49ba3ac12bb5e081dcfb31b5771c6))
75
+ - **cli:** fix search typing, add headless mode and control commands ([506653d](https://github.com/involvex/youtube-music-cli/commit/506653d5e1e7098a87002f039a7051f7f8e7ce76))
76
+ - **layouts:** optimize components with React.memo and responsive padding ([924991c](https://github.com/involvex/youtube-music-cli/commit/924991cc85ae7c96f38760ecce139e58175af8d1))
77
+ - migrate audio player from play-sound to mpv ([ac1aeb3](https://github.com/involvex/youtube-music-cli/commit/ac1aeb3652ec49a5e74c0519d41055e715eb4499))
78
+ - move PlayerControls to MainLayout for global key bindings ([4190bf0](https://github.com/involvex/youtube-music-cli/commit/4190bf0393a4f1c8602d0603a953b36d4351d89d))
79
+ - **player:** Add IPC-based player event monitoring for mpv ([5a40ab0](https://github.com/involvex/youtube-music-cli/commit/5a40ab06679adf6569914075440dce833f1c1226))
80
+ - **ui:** implement responsive layout, adjustable search limit, and fix search navigation ([78150d6](https://github.com/involvex/youtube-music-cli/commit/78150d675746879cce2d329333009cd8cfe8cc4d))
81
+
82
+ ### Performance Improvements
83
+
84
+ - **cli:** optimize UI rendering and fix search result selection ([17e9f7e](https://github.com/involvex/youtube-music-cli/commit/17e9f7efc07980852fa7b1c45e002b2abd93cfd8))
85
+ - memoize view components and remove redundant useEffect in SearchResults ([9b90902](https://github.com/involvex/youtube-music-cli/commit/9b90902d27416df0ca5bd2854e5afb46ab24b128))
86
+ - throttle progress updates and fix exit handler stale closure ([162b732](https://github.com/involvex/youtube-music-cli/commit/162b73292fb21a3eae2cf998a8a02dbb0adc5446))
@@ -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 / Esc" }), " - 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: "p" }), " - Playlists", _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: "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: "D" }), " - Delete Playlist"] }) }), _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 / Esc" }), " - 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: "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: "D" }), " - Delete Playlist"] }) }), _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
  }
@@ -31,5 +31,5 @@ export default function ShortcutsBar() {
31
31
  useKeyBinding(KEYBINDINGS.VOLUME_FINE_UP, volumeFineUp);
32
32
  useKeyBinding(KEYBINDINGS.VOLUME_FINE_DOWN, volumeFineDown);
33
33
  useKeyBinding(KEYBINDINGS.SETTINGS, goConfig);
34
- 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: "n" }), " Next |", ' ', _jsx(Text, { color: theme.colors.text, children: "p" }), " Previous |", ' ', _jsx(Text, { color: theme.colors.text, children: "/" }), " Search |", ' ', _jsx(Text, { color: theme.colors.text, children: "," }), " Settings |", ' ', _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, "%"] })] })] }));
34
+ 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" }), " Previous |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+P" }), " Playlists |", ' ', _jsx(Text, { color: theme.colors.text, children: "p" }), " Plugins |", ' ', _jsx(Text, { color: theme.colors.text, children: "/" }), " Search |", ' ', _jsx(Text, { color: theme.colors.text, children: "," }), " Settings |", ' ', _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, "%"] })] })] }));
35
35
  }
@@ -39,6 +39,9 @@ function MainLayout() {
39
39
  const goToSuggestions = useCallback(() => {
40
40
  dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
41
41
  }, [dispatch]);
42
+ const goToPlugins = useCallback(() => {
43
+ dispatch({ category: 'NAVIGATE', view: VIEW.PLUGINS });
44
+ }, [dispatch]);
42
45
  const goToSettings = useCallback(() => {
43
46
  dispatch({ category: 'NAVIGATE', view: VIEW.SETTINGS });
44
47
  }, [dispatch]);
@@ -69,6 +72,7 @@ function MainLayout() {
69
72
  useKeyBinding(KEYBINDINGS.QUIT, handleQuit);
70
73
  useKeyBinding(KEYBINDINGS.SEARCH, goToSearch);
71
74
  useKeyBinding(KEYBINDINGS.PLAYLISTS, goToPlaylists);
75
+ useKeyBinding(KEYBINDINGS.PLUGINS, goToPlugins);
72
76
  useKeyBinding(KEYBINDINGS.SUGGESTIONS, goToSuggestions);
73
77
  useKeyBinding(KEYBINDINGS.SETTINGS, goToSettings);
74
78
  useKeyBinding(KEYBINDINGS.HELP, goToHelp);
@@ -16,7 +16,10 @@ export default function Suggestions() {
16
16
  const [selectedIndex, setSelectedIndex] = useState(0);
17
17
  useEffect(() => {
18
18
  if (playerState.currentTrack?.videoId) {
19
- getSuggestions(playerState.currentTrack.videoId).then(setSuggestions);
19
+ getSuggestions(playerState.currentTrack.videoId).then(tracks => {
20
+ setSuggestions(tracks);
21
+ setSelectedIndex(0);
22
+ });
20
23
  }
21
24
  }, [playerState.currentTrack?.videoId, getSuggestions]);
22
25
  const navigateUp = useCallback(() => {
@@ -1,11 +1,82 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Playlist list component
3
3
  import { Box, Text } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { useCallback, useState } from 'react';
6
+ import { useNavigation } from "../../hooks/useNavigation.js";
7
+ import { useKeyBinding } from "../../hooks/useKeyboard.js";
8
+ import { usePlayer } from "../../hooks/usePlayer.js";
9
+ import { usePlaylist } from "../../hooks/usePlaylist.js";
4
10
  import { useTheme } from "../../hooks/useTheme.js";
5
- import { getConfigService } from "../../services/config/config.service.js";
11
+ import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
12
+ import { KEYBINDINGS } from "../../utils/constants.js";
6
13
  export default function PlaylistList() {
7
14
  const { theme } = useTheme();
8
- const config = getConfigService();
9
- const playlists = config.get('playlists');
10
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Playlists" }) }), playlists.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No playlists yet" })) : (playlists.map((playlist, index) => (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: theme.colors.primary, children: [index + 1, "."] }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.text, children: playlist.name }), _jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { children: " " }), "(", playlist.tracks?.length || 0, " tracks)"] })] }, playlist.playlistId || index)))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "c" }), " to create playlist", _jsx(Text, { children: " | " }), _jsx(Text, { color: theme.colors.text, children: "Esc" }), " to go back"] }) })] }));
15
+ const { play, setQueue } = usePlayer();
16
+ const { dispatch } = useNavigation();
17
+ const { playlists, createPlaylist, renamePlaylist } = usePlaylist();
18
+ const [selectedIndex, setSelectedIndex] = useState(0);
19
+ const [lastCreated, setLastCreated] = useState(null);
20
+ const [renamingPlaylistId, setRenamingPlaylistId] = useState(null);
21
+ const [renameValue, setRenameValue] = useState('');
22
+ useKeyboardBlocker(renamingPlaylistId !== null);
23
+ const handleCreate = useCallback(() => {
24
+ const name = `Playlist ${playlists.length + 1}`;
25
+ createPlaylist(name);
26
+ setLastCreated(name);
27
+ setSelectedIndex(playlists.length);
28
+ }, [createPlaylist, playlists.length]);
29
+ const navigateUp = useCallback(() => {
30
+ setSelectedIndex(prev => Math.max(0, prev - 1));
31
+ }, []);
32
+ const navigateDown = useCallback(() => {
33
+ setSelectedIndex(prev => Math.min(playlists.length === 0 ? 0 : playlists.length - 1, prev + 1));
34
+ }, [playlists.length]);
35
+ const startPlaylist = useCallback(() => {
36
+ if (renamingPlaylistId)
37
+ return;
38
+ const playlist = playlists[selectedIndex];
39
+ if (!playlist || playlist.tracks.length === 0)
40
+ return;
41
+ setQueue([...playlist.tracks]);
42
+ const firstTrack = playlist.tracks[0];
43
+ if (!firstTrack)
44
+ return;
45
+ play(firstTrack);
46
+ }, [play, playlists, selectedIndex, renamingPlaylistId, setQueue]);
47
+ const handleRename = useCallback(() => {
48
+ const playlist = playlists[selectedIndex];
49
+ if (!playlist)
50
+ return;
51
+ setRenamingPlaylistId(playlist.playlistId);
52
+ setRenameValue(playlist.name);
53
+ }, [playlists, selectedIndex]);
54
+ const handleRenameSubmit = useCallback((value) => {
55
+ if (!renamingPlaylistId)
56
+ return;
57
+ const trimmedValue = value.trim() || `Playlist ${selectedIndex + 1}`;
58
+ renamePlaylist(renamingPlaylistId, trimmedValue);
59
+ setRenamingPlaylistId(null);
60
+ setRenameValue('');
61
+ }, [renamePlaylist, renamingPlaylistId, selectedIndex]);
62
+ const handleBack = useCallback(() => {
63
+ if (renamingPlaylistId) {
64
+ setRenamingPlaylistId(null);
65
+ setRenameValue('');
66
+ return;
67
+ }
68
+ dispatch({ category: 'GO_BACK' });
69
+ }, [dispatch, renamingPlaylistId]);
70
+ useKeyBinding(KEYBINDINGS.UP, navigateUp);
71
+ useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
72
+ useKeyBinding(KEYBINDINGS.SELECT, startPlaylist);
73
+ useKeyBinding(['r'], handleRename);
74
+ useKeyBinding(KEYBINDINGS.CREATE_PLAYLIST, handleCreate);
75
+ useKeyBinding(KEYBINDINGS.BACK, handleBack);
76
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Playlists" }) }), playlists.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No playlists yet" })) : (playlists.map((playlist, index) => {
77
+ const isSelected = index === selectedIndex;
78
+ const isRenaming = renamingPlaylistId === playlist.playlistId && isSelected;
79
+ const rowBackground = isSelected ? theme.colors.secondary : undefined;
80
+ return (_jsxs(Box, { paddingX: 1, backgroundColor: rowBackground, children: [_jsxs(Text, { color: isSelected ? theme.colors.background : theme.colors.primary, bold: isSelected, children: [index + 1, "."] }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [isRenaming ? (_jsx(TextInput, { value: renameValue, onChange: setRenameValue, onSubmit: handleRenameSubmit, placeholder: "Playlist name", focus: true })) : (_jsx(Text, { color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: playlist.name })), _jsx(Text, { color: theme.colors.dim, children: ` (${playlist.tracks.length} tracks)` })] })] }, playlist.playlistId));
81
+ })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.text, children: "Enter" }), " to play playlist |", ' ', _jsx(Text, { color: theme.colors.text, children: "r" }), " to rename |", ' ', _jsx(Text, { color: theme.colors.text, children: "c" }), " to create |", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), " to go back"] }), lastCreated && (_jsxs(Text, { color: theme.colors.accent, children: [" Created ", lastCreated] }))] })] }));
11
82
  }
@@ -6,6 +6,7 @@ import React from 'react';
6
6
  import { SEARCH_TYPE } from "../../utils/constants.js";
7
7
  import { useTheme } from "../../hooks/useTheme.js";
8
8
  import { useKeyBinding } from "../../hooks/useKeyboard.js";
9
+ import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
9
10
  import { Box, Text } from 'ink';
10
11
  import TextInput from 'ink-text-input';
11
12
  import { getConfigService } from "../../services/config/config.service.js";
@@ -46,6 +47,7 @@ function SearchBar({ onInput, isActive = true }) {
46
47
  }, [isActive, onInput]);
47
48
  useKeyBinding(['tab'], cycleType);
48
49
  useKeyBinding(['escape'], clearSearch);
50
+ useKeyboardBlocker(isActive);
49
51
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Type: " }), searchTypes.map((type, index) => (_jsxs(Text, { color: navState.searchType === type
50
52
  ? theme.colors.primary
51
53
  : theme.colors.dim, bold: navState.searchType === type, children: [type, index < searchTypes.length - 1 && ' '] }, type))), _jsx(Text, { color: theme.colors.dim, children: " (Tab to switch)" })] }), isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "Type to search...", focus: isActive })] })), !isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(Text, { color: theme.colors.dim, children: input || 'Type to search...' })] })), _jsx(Text, { color: theme.colors.dim, children: "Type to search, Enter to search, Tab to change type, Esc to clear" })] }));
@@ -11,13 +11,15 @@ import { truncate } from "../../utils/format.js";
11
11
  import { useCallback, useRef, useEffect } from 'react';
12
12
  import { logger } from "../../services/logger/logger.service.js";
13
13
  import { useTerminalSize } from "../../hooks/useTerminalSize.js";
14
+ import { getMusicService } from "../../services/youtube-music/api.js";
14
15
  // Generate unique component instance ID
15
16
  let instanceCounter = 0;
16
17
  function SearchResults({ results, selectedIndex, isActive = true }) {
17
18
  const { theme } = useTheme();
18
19
  const { dispatch } = useNavigation();
19
- const { play } = usePlayer();
20
+ const { play, dispatch: playerDispatch } = usePlayer();
20
21
  const { columns } = useTerminalSize();
22
+ const musicService = getMusicService();
21
23
  // Track component instance and last action time for debouncing
22
24
  const instanceIdRef = useRef(++instanceCounter);
23
25
  const lastSelectTime = useRef(0);
@@ -45,7 +47,7 @@ function SearchResults({ results, selectedIndex, isActive = true }) {
45
47
  }
46
48
  }, [selectedIndex, results.length, dispatch, isActive]);
47
49
  // Play selected result
48
- const playSelected = useCallback(() => {
50
+ const playSelected = useCallback(async () => {
49
51
  logger.debug('SearchResults', 'playSelected called', {
50
52
  isActive,
51
53
  selectedIndex,
@@ -62,12 +64,43 @@ function SearchResults({ results, selectedIndex, isActive = true }) {
62
64
  // Clear queue when playing from search results to ensure indices match
63
65
  play(selected.data, { clearQueue: true });
64
66
  }
67
+ else if (selected && selected.type === 'artist') {
68
+ const artistName = 'name' in selected.data ? selected.data.name : '';
69
+ if (!artistName) {
70
+ logger.warn('SearchResults', 'Artist name missing, cannot search songs');
71
+ return;
72
+ }
73
+ try {
74
+ const response = await musicService.search(artistName, {
75
+ type: 'songs',
76
+ limit: 20,
77
+ });
78
+ const tracks = response.results
79
+ .filter(result => result.type === 'song')
80
+ .map(result => result.data);
81
+ if (tracks.length === 0) {
82
+ logger.warn('SearchResults', 'No songs found for artist', {
83
+ artistName,
84
+ });
85
+ return;
86
+ }
87
+ // Replace queue with artist songs and start playback
88
+ playerDispatch({ category: 'CLEAR_QUEUE' });
89
+ playerDispatch({ category: 'SET_QUEUE', queue: tracks });
90
+ playerDispatch({ category: 'PLAY', track: tracks[0] });
91
+ }
92
+ catch (error) {
93
+ logger.error('SearchResults', 'Failed to play artist songs', {
94
+ error,
95
+ });
96
+ }
97
+ }
65
98
  else {
66
- logger.warn('SearchResults', 'Selected item is not a song', {
99
+ logger.warn('SearchResults', 'Selected item is not playable', {
67
100
  type: selected?.type,
68
101
  });
69
102
  }
70
- }, [selectedIndex, results, play, isActive]);
103
+ }, [selectedIndex, results, play, isActive, musicService, playerDispatch]);
71
104
  // Play selected result handler (memoized to prevent duplicate registrations)
72
105
  const handleSelect = useCallback(() => {
73
106
  const now = Date.now();
@@ -2,6 +2,7 @@
2
2
  import { useCallback, useEffect } from 'react';
3
3
  import { useInput } from 'ink';
4
4
  import { logger } from "../services/logger/logger.service.js";
5
+ import { useKeyboardBlockContext } from "./useKeyboardBlocker.js";
5
6
  // Global registry for key handlers
6
7
  const registry = new Set();
7
8
  /**
@@ -23,7 +24,13 @@ export function useKeyBinding(keys, handler) {
23
24
  * This should be rendered once at the root of the app.
24
25
  */
25
26
  export function KeyboardManager() {
27
+ const { blockCount } = useKeyboardBlockContext();
26
28
  useInput((input, key) => {
29
+ if (blockCount > 0) {
30
+ // When keyboard input is blocked (e.g., within a focused text input),
31
+ // we deliberately skip executing global shortcuts.
32
+ return;
33
+ }
27
34
  // Debug logging for key presses (helps diagnose binding issues)
28
35
  if (input || key.ctrl || key.meta || key.shift) {
29
36
  logger.debug('KeyboardManager', 'Key pressed', {
@@ -74,6 +81,8 @@ export function KeyboardManager() {
74
81
  return false;
75
82
  if (hasShift && !key.shift)
76
83
  return false;
84
+ if (!hasShift && key.shift)
85
+ return false;
77
86
  // Check the actual key
78
87
  if (mainKey === 'up' && key.upArrow)
79
88
  return true;
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ type KeyboardBlockContextValue = {
3
+ blockCount: number;
4
+ increment: () => void;
5
+ decrement: () => void;
6
+ };
7
+ export declare function KeyboardBlockProvider({ children }: {
8
+ children: ReactNode;
9
+ }): import("react/jsx-runtime").JSX.Element;
10
+ export declare function useKeyboardBlockContext(): KeyboardBlockContextValue;
11
+ export declare function useKeyboardBlocker(shouldBlock: boolean): void;
12
+ export declare function useIsKeyboardBlocked(): boolean;
13
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
+ const KeyboardBlockContext = createContext(null);
4
+ export function KeyboardBlockProvider({ children }) {
5
+ const [blockCount, setBlockCount] = useState(0);
6
+ const increment = useCallback(() => {
7
+ setBlockCount(prev => prev + 1);
8
+ }, []);
9
+ const decrement = useCallback(() => {
10
+ setBlockCount(prev => Math.max(0, prev - 1));
11
+ }, []);
12
+ const value = useMemo(() => ({ blockCount, increment, decrement }), [blockCount, increment, decrement]);
13
+ return (_jsx(KeyboardBlockContext.Provider, { value: value, children: children }));
14
+ }
15
+ export function useKeyboardBlockContext() {
16
+ const context = useContext(KeyboardBlockContext);
17
+ if (!context) {
18
+ throw new Error('useKeyboardBlockContext must be used within KeyboardBlockProvider');
19
+ }
20
+ return context;
21
+ }
22
+ export function useKeyboardBlocker(shouldBlock) {
23
+ const { increment, decrement } = useKeyboardBlockContext();
24
+ const blockedRef = useRef(false);
25
+ useEffect(() => {
26
+ if (shouldBlock && !blockedRef.current) {
27
+ increment();
28
+ blockedRef.current = true;
29
+ }
30
+ else if (!shouldBlock && blockedRef.current) {
31
+ decrement();
32
+ blockedRef.current = false;
33
+ }
34
+ return () => {
35
+ if (blockedRef.current) {
36
+ decrement();
37
+ blockedRef.current = false;
38
+ }
39
+ };
40
+ }, [shouldBlock, increment, decrement]);
41
+ }
42
+ export function useIsKeyboardBlocked() {
43
+ const { blockCount } = useKeyboardBlockContext();
44
+ return blockCount > 0;
45
+ }
@@ -4,6 +4,7 @@ export declare function usePlaylist(): {
4
4
  playlists: Playlist[];
5
5
  createPlaylist: (name: string) => void;
6
6
  deletePlaylist: (playlistId: string) => void;
7
+ renamePlaylist: (playlistId: string, newName: string) => void;
7
8
  addTrackToPlaylist: (playlistId: string, track: Track, force?: boolean) => AddTrackResult;
8
9
  removeTrackFromPlaylist: (playlistId: string, trackIndex: number) => void;
9
10
  };
@@ -37,6 +37,13 @@ export function usePlaylist() {
37
37
  configService.set('playlists', updatedPlaylists);
38
38
  return 'added';
39
39
  }, [playlists, configService]);
40
+ const renamePlaylist = useCallback((playlistId, newName) => {
41
+ const updatedPlaylists = playlists.map(playlist => playlist.playlistId === playlistId
42
+ ? { ...playlist, name: newName }
43
+ : playlist);
44
+ setPlaylists(updatedPlaylists);
45
+ configService.set('playlists', updatedPlaylists);
46
+ }, [playlists, configService]);
40
47
  const removeTrackFromPlaylist = useCallback((playlistId, trackIndex) => {
41
48
  const playlistIndex = playlists.findIndex(p => p.playlistId === playlistId);
42
49
  if (playlistIndex === -1)
@@ -50,6 +57,7 @@ export function usePlaylist() {
50
57
  playlists,
51
58
  createPlaylist,
52
59
  deletePlaylist,
60
+ renamePlaylist,
53
61
  addTrackToPlaylist,
54
62
  removeTrackFromPlaylist,
55
63
  };
@@ -7,6 +7,7 @@ import { ThemeProvider } from "./contexts/theme.context.js";
7
7
  import { PlayerProvider } from "./stores/player.store.js";
8
8
  import { ErrorBoundary } from "./components/common/ErrorBoundary.js";
9
9
  import { KeyboardManager } from "./hooks/useKeyboard.js";
10
+ import { KeyboardBlockProvider } from "./hooks/useKeyboardBlocker.js";
10
11
  import { Box, Text } from 'ink';
11
12
  import { useEffect } from 'react';
12
13
  import { useNavigation } from "./hooks/useNavigation.js";
@@ -65,5 +66,5 @@ function HeadlessLayout({ flags }) {
65
66
  return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "green", children: "Headless mode active." }) }));
66
67
  }
67
68
  export default function Main({ flags }) {
68
- return (_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { children: _jsx(PlayerProvider, { children: _jsx(NavigationProvider, { children: _jsx(PluginsProvider, { children: _jsxs(Box, { flexDirection: "column", children: [_jsx(KeyboardManager, {}), flags?.headless ? (_jsx(HeadlessLayout, { flags: flags })) : (_jsxs(_Fragment, { children: [_jsx(Initializer, { flags: flags }), _jsx(MainLayout, {})] }))] }) }) }) }) }) }));
69
+ return (_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { children: _jsx(PlayerProvider, { children: _jsx(NavigationProvider, { children: _jsx(PluginsProvider, { children: _jsx(KeyboardBlockProvider, { children: _jsxs(Box, { flexDirection: "column", children: [_jsx(KeyboardManager, {}), flags?.headless ? (_jsx(HeadlessLayout, { flags: flags })) : (_jsxs(_Fragment, { children: [_jsx(Initializer, { flags: flags }), _jsx(MainLayout, {})] }))] }) }) }) }) }) }) }));
69
70
  }
@@ -30,9 +30,10 @@ export declare const KEYBINDINGS: {
30
30
  readonly QUIT: readonly ["q", "escape"];
31
31
  readonly HELP: readonly ["?"];
32
32
  readonly SEARCH: readonly ["/"];
33
- readonly PLAYLISTS: readonly ["p"];
33
+ readonly PLAYLISTS: readonly ["shift+p"];
34
34
  readonly SUGGESTIONS: readonly ["g"];
35
35
  readonly SETTINGS: readonly [","];
36
+ readonly PLUGINS: readonly ["p"];
36
37
  readonly PLAY_PAUSE: readonly [" "];
37
38
  readonly NEXT: readonly ["n", "right"];
38
39
  readonly PREVIOUS: readonly ["b", "left"];
@@ -38,9 +38,10 @@ export const KEYBINDINGS = {
38
38
  QUIT: ['q', 'escape'],
39
39
  HELP: ['?'],
40
40
  SEARCH: ['/'],
41
- PLAYLISTS: ['p'],
41
+ PLAYLISTS: ['shift+p'],
42
42
  SUGGESTIONS: ['g'],
43
43
  SETTINGS: [','],
44
+ PLUGINS: ['p'],
44
45
  // Player
45
46
  PLAY_PAUSE: [' '],
46
47
  NEXT: ['n', 'right'],
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.8",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,11 @@
15
15
  "ymc": "dist/source/cli.js"
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "LICENSE",
22
+ ".github/FUNDING.yml"
19
23
  ],
20
24
  "scripts": {
21
25
  "prebuild": "bun run format && bun run lint:fix && bun run typecheck",
@@ -29,27 +33,24 @@
29
33
  "lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern dist",
30
34
  "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix --ignore-pattern dist",
31
35
  "start": "bun run dist/source/cli.js",
32
- "test": "prettier --check . && xo && ava",
36
+ "test": "prettier --check . && bun run lint && ava",
33
37
  "typecheck": "tsc --noEmit",
34
38
  "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
35
39
  "clean": "rimraf dist",
36
40
  "release": "powershell -File scripts/release.ps1"
37
41
  },
38
42
  "xo": {
39
- "extends": "xo-react",
43
+ "react": true,
40
44
  "prettier": true,
41
45
  "rules": {
42
46
  "react/prop-types": "off"
43
- }
47
+ },
48
+ "semicolon": true
44
49
  },
45
50
  "prettier": "@vdemedes/prettier-config",
46
51
  "ava": {
47
- "extensions": {
48
- "ts": "module",
49
- "tsx": "module"
50
- },
51
- "nodeArguments": [
52
- "--loader=ts-node/esm"
52
+ "files": [
53
+ "tests/**/*.js"
53
54
  ]
54
55
  },
55
56
  "dependencies": {
@@ -61,6 +62,7 @@
61
62
  "jiti": "^2.6.1",
62
63
  "meow": "^14.0.0",
63
64
  "node-notifier": "^10.0.1",
65
+ "discord-rpc": "^4.0.1",
64
66
  "node-youtube-music": "^0.10.3",
65
67
  "play-sound": "^1.1.6",
66
68
  "react": "^19.2.4",
package/dist/test.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/test.js DELETED
@@ -1,13 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render } from 'ink-testing-library';
3
- import App from './source/app.js';
4
- import chalk from 'chalk';
5
- import test from 'ava';
6
- test('greet unknown user', t => {
7
- const { lastFrame } = render(_jsx(App, { flags: { help: true } }));
8
- t.is(lastFrame(), `Hello, ${chalk.green('Stranger')}`);
9
- });
10
- test('greet user with a name', t => {
11
- const { lastFrame } = render(_jsx(App, { flags: { version: true } }));
12
- t.is(lastFrame(), `Hello, ${chalk.green('Jane')}`);
13
- });