@involvex/youtube-music-cli 0.0.13 → 0.0.15
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/layouts/SearchLayout.js +2 -2
- package/dist/source/components/search/SearchBar.js +2 -2
- package/dist/source/components/search/SearchResults.js +9 -12
- 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 +1 -1
- package/readme.md +39 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [0.0.15](https://github.com/involvex/youtube-music-cli/compare/v0.0.14...v0.0.15) (2026-02-18)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **ui:** remove escape key from quit keybinding ([df6e794](https://github.com/involvex/youtube-music-cli/commit/df6e794583aa96a1dd27b1ff208f3f1b69249829))
|
|
6
|
+
|
|
7
|
+
### BREAKING CHANGES
|
|
8
|
+
|
|
9
|
+
- **ui:** 'escape' no longer quits; use 'q' instead.
|
|
10
|
+
|
|
11
|
+
## [0.0.14](https://github.com/involvex/youtube-music-cli/compare/v0.0.13...v0.0.14) (2026-02-18)
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **download:** add download feature with configuration and shortcuts ([a616c4c](https://github.com/involvex/youtube-music-cli/commit/a616c4c2443cb6f7d929268fd08453708255503c))
|
|
16
|
+
|
|
1
17
|
## [0.0.13](https://github.com/involvex/youtube-music-cli/compare/v0.0.12...v0.0.13) (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
|
|
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: "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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } 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";
|
|
@@ -108,7 +108,7 @@ function SearchLayout() {
|
|
|
108
108
|
dispatch({ category: 'SET_SEARCH_QUERY', query: '' });
|
|
109
109
|
};
|
|
110
110
|
}, [dispatch]);
|
|
111
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
111
|
+
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 => {
|
|
112
112
|
void performSearch(input);
|
|
113
113
|
} }), (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
|
|
114
114
|
? 'Type to search, Enter to start, Esc to clear'
|
|
@@ -48,8 +48,8 @@ function SearchBar({ onInput, isActive = true }) {
|
|
|
48
48
|
useKeyBinding(['tab'], cycleType);
|
|
49
49
|
useKeyBinding(['escape'], clearSearch);
|
|
50
50
|
useKeyboardBlocker(isActive);
|
|
51
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary,
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.dim, children: "Type: " }), searchTypes.map((type, index) => (_jsxs(Text, { color: navState.searchType === type
|
|
52
52
|
? theme.colors.primary
|
|
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...' })] }))
|
|
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...' })] }))] }));
|
|
54
54
|
}
|
|
55
55
|
export default React.memo(SearchBar);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Search results component
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
@@ -7,7 +7,7 @@ import { useNavigation } from "../../hooks/useNavigation.js";
|
|
|
7
7
|
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
8
8
|
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
9
9
|
import { usePlaylist } from "../../hooks/usePlaylist.js";
|
|
10
|
-
import { KEYBINDINGS
|
|
10
|
+
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
11
11
|
import { truncate } from "../../utils/format.js";
|
|
12
12
|
import { useCallback, useRef, useEffect, useState } from 'react';
|
|
13
13
|
import { logger } from "../../services/logger/logger.service.js";
|
|
@@ -211,12 +211,9 @@ function SearchResults({ results, selectedIndex, isActive = true, onMixCreated,
|
|
|
211
211
|
if (firstTrack) {
|
|
212
212
|
playerDispatch({ category: 'PLAY', track: firstTrack });
|
|
213
213
|
}
|
|
214
|
-
|
|
215
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.PLAYER });
|
|
216
|
-
mixCreatedRef.current?.(`Created mix "${playlist.name}" with ${uniqueTracks.length} tracks — playing now.`);
|
|
214
|
+
mixCreatedRef.current?.(`Created mix "${playlist.name}" with ${uniqueTracks.length} tracks — playing now (Esc to go back).`);
|
|
217
215
|
}, [
|
|
218
216
|
createPlaylist,
|
|
219
|
-
dispatch,
|
|
220
217
|
isActive,
|
|
221
218
|
musicService,
|
|
222
219
|
playerDispatch,
|
|
@@ -273,11 +270,11 @@ function SearchResults({ results, selectedIndex, isActive = true, onMixCreated,
|
|
|
273
270
|
}
|
|
274
271
|
// Calculate responsive truncation
|
|
275
272
|
const maxTitleWidth = Math.max(20, Math.floor(columns * 0.4));
|
|
276
|
-
return (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
273
|
+
return (_jsx(Box, { flexDirection: "column", children: results.map((result, index) => {
|
|
274
|
+
const isSelected = index === selectedIndex;
|
|
275
|
+
const data = result.data;
|
|
276
|
+
const title = 'title' in data ? data.title : 'name' in data ? data.name : 'Unknown';
|
|
277
|
+
return (_jsxs(Box, { paddingX: 1, backgroundColor: isSelected ? theme.colors.secondary : undefined, children: [_jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.dim, bold: isSelected, children: (isSelected ? '> ' : ' ') + (index + 1).toString().padEnd(4) }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.dim, bold: isSelected, children: result.type.toUpperCase().padEnd(10) }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: truncate(title, maxTitleWidth) })] }, index));
|
|
278
|
+
}) }));
|
|
282
279
|
}
|
|
283
280
|
export default React.memo(SearchResults);
|
|
@@ -27,7 +27,7 @@ export declare const SEARCH_TYPE: {
|
|
|
27
27
|
readonly PLAYLISTS: "playlists";
|
|
28
28
|
};
|
|
29
29
|
export declare const KEYBINDINGS: {
|
|
30
|
-
readonly QUIT: readonly ["q"
|
|
30
|
+
readonly QUIT: readonly ["q"];
|
|
31
31
|
readonly HELP: readonly ["?"];
|
|
32
32
|
readonly SEARCH: readonly ["/"];
|
|
33
33
|
readonly PLAYLISTS: readonly ["shift+p"];
|
|
Binary file
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -25,6 +25,8 @@ A powerful Terminal User Interface (TUI) music player for YouTube Music
|
|
|
25
25
|
- 🔌 **Plugin System** - Extend functionality with plugins
|
|
26
26
|
- ⌨️ **Keyboard-Driven** - Efficient vim-style navigation
|
|
27
27
|
- 🖥️ **Headless Mode** - Run without TUI for scripting
|
|
28
|
+
- 💾 **Downloads** - Save tracks/playlists/artists with `Shift+D`
|
|
29
|
+
- 🏷️ **Metadata Tagging** - Auto-tag title/artist/album with optional cover art
|
|
28
30
|
|
|
29
31
|
## Prerequisites
|
|
30
32
|
|
|
@@ -161,15 +163,15 @@ youtube-music-cli play dQw4w9WgXcQ --shuffle
|
|
|
161
163
|
|
|
162
164
|
### Global
|
|
163
165
|
|
|
164
|
-
| Key
|
|
165
|
-
|
|
|
166
|
-
| `?`
|
|
167
|
-
| `/`
|
|
168
|
-
| `p`
|
|
169
|
-
| `g`
|
|
170
|
-
| `,`
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
166
|
+
| Key | Action |
|
|
167
|
+
| ----- | --------------- |
|
|
168
|
+
| `?` | Show help |
|
|
169
|
+
| `/` | Search |
|
|
170
|
+
| `p` | Plugins manager |
|
|
171
|
+
| `g` | Suggestions |
|
|
172
|
+
| `,` | Settings |
|
|
173
|
+
| `Esc` | Go back |
|
|
174
|
+
| `q` | Quit |
|
|
173
175
|
|
|
174
176
|
### Playback
|
|
175
177
|
|
|
@@ -194,6 +196,12 @@ youtube-music-cli play dQw4w9WgXcQ --shuffle
|
|
|
194
196
|
| `Enter` | Select |
|
|
195
197
|
| `Esc` | Back |
|
|
196
198
|
|
|
199
|
+
### Downloads
|
|
200
|
+
|
|
201
|
+
| Key | Action |
|
|
202
|
+
| --------- | ------------------------------------------------------- |
|
|
203
|
+
| `Shift+D` | Download selected song/artist/playlist or playlist view |
|
|
204
|
+
|
|
197
205
|
## Plugins
|
|
198
206
|
|
|
199
207
|
Extend youtube-music-cli with plugins!
|
|
@@ -259,7 +267,10 @@ Config is stored in `~/.youtube-music-cli/config.json`:
|
|
|
259
267
|
"volume": 70,
|
|
260
268
|
"shuffle": false,
|
|
261
269
|
"repeat": "off",
|
|
262
|
-
"streamQuality": "high"
|
|
270
|
+
"streamQuality": "high",
|
|
271
|
+
"downloadsEnabled": false,
|
|
272
|
+
"downloadDirectory": "D:/Music/youtube-music-cli",
|
|
273
|
+
"downloadFormat": "mp3"
|
|
263
274
|
}
|
|
264
275
|
```
|
|
265
276
|
|
|
@@ -271,6 +282,15 @@ Config is stored in `~/.youtube-music-cli/config.json`:
|
|
|
271
282
|
| `medium` | 128kbps - Balanced |
|
|
272
283
|
| `high` | 256kbps+ - Best quality |
|
|
273
284
|
|
|
285
|
+
### Download Settings
|
|
286
|
+
|
|
287
|
+
- Enable/disable downloads in **Settings** (`,`).
|
|
288
|
+
- Set your download directory in **Settings → Download Folder**.
|
|
289
|
+
- Choose format in **Settings → Download Format** (`mp3` or `m4a`).
|
|
290
|
+
- Downloads are saved as:
|
|
291
|
+
- `<downloadDirectory>/<artist>/<album>/<title>.mp3` (or `.m4a`)
|
|
292
|
+
- MP3/M4A files are tagged with metadata (`title`, `artist`, `album`) and include cover art when available.
|
|
293
|
+
|
|
274
294
|
## Troubleshooting
|
|
275
295
|
|
|
276
296
|
### mpv not found
|
|
@@ -289,7 +309,7 @@ mpv --version
|
|
|
289
309
|
|
|
290
310
|
### TUI rendering issues
|
|
291
311
|
|
|
292
|
-
|
|
312
|
+
If rendering looks wrong, try resizing your terminal window or restarting the app.
|
|
293
313
|
|
|
294
314
|
### Plugin not loading
|
|
295
315
|
|
|
@@ -350,3 +370,11 @@ MIT © [Involvex](https://github.com/involvex)
|
|
|
350
370
|
Made with ❤️ for music lovers
|
|
351
371
|
|
|
352
372
|
</div>
|
|
373
|
+
|
|
374
|
+
## Supporting
|
|
375
|
+
|
|
376
|
+
**[☕ Buymeacoffee](https://buymeacoffee.com/involvex)**
|
|
377
|
+
|
|
378
|
+
**[🪙 Paypal](https://paypal.me/involvex)**
|
|
379
|
+
|
|
380
|
+
**⌨️ [Github Sponsors](https://github.com/sponsors/involvex)**
|