@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 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 / 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: "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"] })] })] }));
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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", gap: 1, children: [_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: "Search" }), _jsxs(Text, { color: theme.colors.dim, children: [' ', "| Limit: ", navState.searchLimit, " (Use [ or ] to adjust)"] })] }), _jsx(SearchBar, { isActive: isTyping && !isSearching, onInput: input => {
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, 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
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...' })] })), _jsx(Text, { color: theme.colors.dim, children: "Type to search, Enter to search, Tab to change type, Esc to clear" })] }));
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 { 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 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, VIEW } from "../../utils/constants.js";
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
- // Navigate to player view so the user lands on the queue/player
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 (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: theme.colors.dim, bold: true, children: ["Results (", results.length, ")"] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.dim, bold: true, children: ['#'.padEnd(6), " ", 'Type'.padEnd(10), " ", 'Title'.padEnd(maxTitleWidth)] }) }), results.map((result, index) => {
277
- const isSelected = index === selectedIndex;
278
- const data = result.data;
279
- const title = 'title' in data ? data.title : 'name' in data ? data.name : 'Unknown';
280
- return (_jsxs(Box, { paddingX: 1, borderStyle: isSelected ? 'double' : undefined, borderColor: isSelected ? theme.colors.primary : 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));
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", "escape"];
30
+ readonly QUIT: readonly ["q"];
31
31
  readonly HELP: readonly ["?"];
32
32
  readonly SEARCH: readonly ["/"];
33
33
  readonly PLAYLISTS: readonly ["shift+p"];
@@ -35,7 +35,7 @@ export const SEARCH_TYPE = {
35
35
  // Keybindings
36
36
  export const KEYBINDINGS = {
37
37
  // Global
38
- QUIT: ['q', 'escape'],
38
+ QUIT: ['q'],
39
39
  HELP: ['?'],
40
40
  SEARCH: ['/'],
41
41
  PLAYLISTS: ['shift+p'],
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
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 | Action |
165
- | ----------- | --------------- |
166
- | `?` | Show help |
167
- | `/` | Search |
168
- | `p` | Plugins manager |
169
- | `g` | Suggestions |
170
- | `,` | Settings |
171
- | `q` / `Esc` | Quit / Go back |
172
- | `Ctrl+L` | Refresh screen |
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
- Press `Ctrl+L` to refresh the screen, or try a different terminal emulator.
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)**