@involvex/youtube-music-cli 0.0.44 → 0.0.45

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,9 @@
1
+ ## [0.0.45](https://github.com/involvex/youtube-music-cli/compare/v0.0.44...v0.0.45) (2026-02-24)
2
+
3
+ ### Features
4
+
5
+ - add volume fade duration setting for smoother playback transitions ([936e3e2](https://github.com/involvex/youtube-music-cli/commit/936e3e2c7ae043b26dd48a1cd6f2d26cb11b0ae2))
6
+
1
7
  ## [0.0.44](https://github.com/involvex/youtube-music-cli/compare/v0.0.43...v0.0.44) (2026-02-23)
2
8
 
3
9
  ### Features
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.44",
3
+ "version": "0.0.45",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -157,6 +157,7 @@ async function runDirectPlaybackCommand(flags) {
157
157
  const playbackOptions = {
158
158
  volume: flags.volume ?? config.get('volume'),
159
159
  audioNormalization: config.get('audioNormalization'),
160
+ volumeFadeDuration: config.get('volumeFadeDuration'),
160
161
  };
161
162
  let track;
162
163
  if (flags.playTrack) {
@@ -21,11 +21,13 @@ const EQUALIZER_PRESETS = [
21
21
  'bright',
22
22
  'warm',
23
23
  ];
24
+ const VOLUME_FADE_PRESETS = [0, 1, 2, 3, 5];
24
25
  const SETTINGS_ITEMS = [
25
26
  'Stream Quality',
26
27
  'Audio Normalization',
27
28
  'Gapless Playback',
28
29
  'Crossfade Duration',
30
+ 'Volume Fade Duration',
29
31
  'Equalizer Preset',
30
32
  'Notifications',
31
33
  'Discord Rich Presence',
@@ -47,6 +49,7 @@ export default function Settings() {
47
49
  const [audioNormalization, setAudioNormalization] = useState(config.get('audioNormalization') ?? false);
48
50
  const [gaplessPlayback, setGaplessPlayback] = useState(config.get('gaplessPlayback') ?? true);
49
51
  const [crossfadeDuration, setCrossfadeDuration] = useState(config.get('crossfadeDuration') ?? 0);
52
+ const [volumeFadeDuration, setVolumeFadeDuration] = useState(config.get('volumeFadeDuration') ?? 0);
50
53
  const [equalizerPreset, setEqualizerPreset] = useState(config.get('equalizerPreset') ?? 'flat');
51
54
  const [notifications, setNotifications] = useState(config.get('notifications') ?? false);
52
55
  const [discordRpc, setDiscordRpc] = useState(config.get('discordRichPresence') ?? false);
@@ -85,6 +88,13 @@ export default function Settings() {
85
88
  setCrossfadeDuration(next);
86
89
  config.set('crossfadeDuration', next);
87
90
  };
91
+ const cycleVolumeFadeDuration = () => {
92
+ const currentIndex = VOLUME_FADE_PRESETS.indexOf(volumeFadeDuration);
93
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % VOLUME_FADE_PRESETS.length;
94
+ const next = VOLUME_FADE_PRESETS[nextIndex] ?? 0;
95
+ setVolumeFadeDuration(next);
96
+ config.set('volumeFadeDuration', next);
97
+ };
88
98
  const cycleEqualizerPreset = () => {
89
99
  const currentIndex = EQUALIZER_PRESETS.indexOf(equalizerPreset);
90
100
  const nextPreset = EQUALIZER_PRESETS[(currentIndex + 1) % EQUALIZER_PRESETS.length];
@@ -142,36 +152,39 @@ export default function Settings() {
142
152
  cycleCrossfadeDuration();
143
153
  }
144
154
  else if (selectedIndex === 4) {
145
- cycleEqualizerPreset();
155
+ cycleVolumeFadeDuration();
146
156
  }
147
157
  else if (selectedIndex === 5) {
148
- toggleNotifications();
158
+ cycleEqualizerPreset();
149
159
  }
150
160
  else if (selectedIndex === 6) {
151
- toggleDiscordRpc();
161
+ toggleNotifications();
152
162
  }
153
163
  else if (selectedIndex === 7) {
154
- toggleDownloadsEnabled();
164
+ toggleDiscordRpc();
155
165
  }
156
166
  else if (selectedIndex === 8) {
157
- setIsEditingDownloadDirectory(true);
167
+ toggleDownloadsEnabled();
158
168
  }
159
169
  else if (selectedIndex === 9) {
160
- cycleDownloadFormat();
170
+ setIsEditingDownloadDirectory(true);
161
171
  }
162
172
  else if (selectedIndex === 10) {
163
- cycleSleepTimer();
173
+ cycleDownloadFormat();
164
174
  }
165
175
  else if (selectedIndex === 11) {
166
- dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
176
+ cycleSleepTimer();
167
177
  }
168
178
  else if (selectedIndex === 12) {
169
- dispatch({ category: 'NAVIGATE', view: VIEW.EXPORT_PLAYLISTS });
179
+ dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
170
180
  }
171
181
  else if (selectedIndex === 13) {
172
- dispatch({ category: 'NAVIGATE', view: VIEW.KEYBINDINGS });
182
+ dispatch({ category: 'NAVIGATE', view: VIEW.EXPORT_PLAYLISTS });
173
183
  }
174
184
  else if (selectedIndex === 14) {
185
+ dispatch({ category: 'NAVIGATE', view: VIEW.KEYBINDINGS });
186
+ }
187
+ else if (selectedIndex === 15) {
175
188
  dispatch({ category: 'NAVIGATE', view: VIEW.PLUGINS });
176
189
  }
177
190
  };
@@ -181,7 +194,7 @@ export default function Settings() {
181
194
  const sleepTimerLabel = isActive && remainingSeconds !== null
182
195
  ? `Sleep Timer: ${formatTime(remainingSeconds)} remaining (Enter to cancel)`
183
196
  : 'Sleep Timer: Off (Enter to set)';
184
- 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: "Settings" }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 0 ? theme.colors.primary : undefined, color: selectedIndex === 0 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 0, children: ["Stream Quality: ", quality.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 1 ? theme.colors.primary : undefined, color: selectedIndex === 1 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 1, children: ["Audio Normalization: ", audioNormalization ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 2 ? theme.colors.primary : undefined, color: selectedIndex === 2 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 2, children: ["Gapless Playback: ", gaplessPlayback ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 3 ? theme.colors.primary : undefined, color: selectedIndex === 3 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 3, children: ["Crossfade: ", crossfadeDuration === 0 ? 'Off' : `${crossfadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 4 ? theme.colors.primary : undefined, color: selectedIndex === 4 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 4, children: ["Equalizer: ", formatEqualizerLabel(equalizerPreset)] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 5 ? theme.colors.primary : undefined, color: selectedIndex === 5 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 5, children: ["Desktop Notifications: ", notifications ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 6 ? theme.colors.primary : undefined, color: selectedIndex === 6 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 6, children: ["Discord Rich Presence: ", discordRpc ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 7 ? theme.colors.primary : undefined, color: selectedIndex === 7 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 7, children: ["Download Feature: ", downloadsEnabled ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: isEditingDownloadDirectory && selectedIndex === 8 ? (_jsx(TextInput, { value: downloadDirectory, onChange: setDownloadDirectory, onSubmit: value => {
197
+ 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: "Settings" }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 0 ? theme.colors.primary : undefined, color: selectedIndex === 0 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 0, children: ["Stream Quality: ", quality.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 1 ? theme.colors.primary : undefined, color: selectedIndex === 1 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 1, children: ["Audio Normalization: ", audioNormalization ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 2 ? theme.colors.primary : undefined, color: selectedIndex === 2 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 2, children: ["Gapless Playback: ", gaplessPlayback ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 3 ? theme.colors.primary : undefined, color: selectedIndex === 3 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 3, children: ["Crossfade: ", crossfadeDuration === 0 ? 'Off' : `${crossfadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 4 ? theme.colors.primary : undefined, color: selectedIndex === 4 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 4, children: ["Volume Fade:", ' ', volumeFadeDuration === 0 ? 'Off' : `${volumeFadeDuration}s`] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 5 ? theme.colors.primary : undefined, color: selectedIndex === 5 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 5, children: ["Equalizer: ", formatEqualizerLabel(equalizerPreset)] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 6 ? theme.colors.primary : undefined, color: selectedIndex === 6 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 6, children: ["Desktop Notifications: ", notifications ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 7 ? theme.colors.primary : undefined, color: selectedIndex === 7 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 7, children: ["Discord Rich Presence: ", discordRpc ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 8 ? theme.colors.primary : undefined, color: selectedIndex === 8 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 8, children: ["Download Feature: ", downloadsEnabled ? 'ON' : 'OFF'] }) }), _jsx(Box, { paddingX: 1, children: isEditingDownloadDirectory && selectedIndex === 9 ? (_jsx(TextInput, { value: downloadDirectory, onChange: setDownloadDirectory, onSubmit: value => {
185
198
  const normalized = value.trim();
186
199
  if (!normalized) {
187
200
  setIsEditingDownloadDirectory(false);
@@ -190,9 +203,9 @@ export default function Settings() {
190
203
  setDownloadDirectory(normalized);
191
204
  config.set('downloadDirectory', normalized);
192
205
  setIsEditingDownloadDirectory(false);
193
- }, placeholder: "Download directory", focus: true })) : (_jsxs(Text, { backgroundColor: selectedIndex === 8 ? theme.colors.primary : undefined, color: selectedIndex === 8 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 8, children: ["Download Folder: ", downloadDirectory] })) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 9 ? theme.colors.primary : undefined, color: selectedIndex === 9 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 9, children: ["Download Format: ", downloadFormat.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 10 ? theme.colors.primary : undefined, color: selectedIndex === 10
206
+ }, placeholder: "Download directory", focus: true })) : (_jsxs(Text, { backgroundColor: selectedIndex === 9 ? theme.colors.primary : undefined, color: selectedIndex === 9 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 9, children: ["Download Folder: ", downloadDirectory] })) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedIndex === 10 ? theme.colors.primary : undefined, color: selectedIndex === 10 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 10, children: ["Download Format: ", downloadFormat.toUpperCase()] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 11 ? theme.colors.primary : undefined, color: selectedIndex === 11
194
207
  ? theme.colors.background
195
208
  : isActive
196
209
  ? theme.colors.accent
197
- : theme.colors.text, bold: selectedIndex === 10, children: sleepTimerLabel }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 11 ? theme.colors.primary : undefined, color: selectedIndex === 11 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 11, children: "Import Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 12 ? theme.colors.primary : undefined, color: selectedIndex === 12 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 12, children: "Export Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 13 ? theme.colors.primary : undefined, color: selectedIndex === 13 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 13, children: "Custom Keybindings \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 14 ? theme.colors.primary : undefined, color: selectedIndex === 14 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 14, children: "Manage Plugins" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Arrows to navigate, Enter to select, Esc/q to go back" }) })] }));
210
+ : theme.colors.text, bold: selectedIndex === 11, children: sleepTimerLabel }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 12 ? theme.colors.primary : undefined, color: selectedIndex === 12 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 12, children: "Import Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 13 ? theme.colors.primary : undefined, color: selectedIndex === 13 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 13, children: "Export Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 14 ? theme.colors.primary : undefined, color: selectedIndex === 14 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 14, children: "Custom Keybindings \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 15 ? theme.colors.primary : undefined, color: selectedIndex === 15 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 15, children: "Manage Plugins" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Arrows to navigate, Enter to select, Esc/q to go back" }) })] }));
198
211
  }
@@ -43,6 +43,7 @@ class ConfigService {
43
43
  },
44
44
  gaplessPlayback: true,
45
45
  crossfadeDuration: 0,
46
+ volumeFadeDuration: 0,
46
47
  equalizerPreset: 'flat',
47
48
  };
48
49
  }
@@ -6,6 +6,7 @@ export type PlayOptions = {
6
6
  gaplessPlayback?: boolean;
7
7
  crossfadeDuration?: number;
8
8
  equalizerPreset?: EqualizerPreset;
9
+ volumeFadeDuration?: number;
9
10
  };
10
11
  export type MpvArgsOptions = PlayOptions & {
11
12
  volume: number;
@@ -5,11 +5,16 @@ import { logger } from "../logger/logger.service.js";
5
5
  export function buildMpvArgs(url, ipcPath, options) {
6
6
  const gapless = options.gaplessPlayback ?? true;
7
7
  const crossfadeDuration = Math.max(0, options.crossfadeDuration ?? 0);
8
+ const fadeDuration = Math.max(0, options.volumeFadeDuration ?? 0);
8
9
  const eqPreset = options.equalizerPreset ?? 'flat';
9
10
  const audioFilters = [];
10
11
  if (options.audioNormalization) {
11
12
  audioFilters.push('dynaudnorm');
12
13
  }
14
+ if (fadeDuration > 0) {
15
+ audioFilters.push(`afade=t=in:st=0:d=${fadeDuration}`);
16
+ audioFilters.push(`afade=t=out:d=${fadeDuration}`);
17
+ }
13
18
  if (crossfadeDuration > 0) {
14
19
  audioFilters.push(`acrossfade=d=${crossfadeDuration}`);
15
20
  }
@@ -141,6 +141,7 @@ class WebServerManager {
141
141
  handleCommand(action) {
142
142
  logger.debug('WebServerManager', 'Executing command from client', { action });
143
143
  const playerService = getPlayerService();
144
+ const config = getConfigService();
144
145
  // Execute command and update internal state
145
146
  switch (action.category) {
146
147
  case 'PLAY': {
@@ -152,6 +153,7 @@ class WebServerManager {
152
153
  const youtubeUrl = `https://www.youtube.com/watch?v=${action.track.videoId}`;
153
154
  void playerService.play(youtubeUrl, {
154
155
  volume: this.internalState.volume,
156
+ volumeFadeDuration: config.get('volumeFadeDuration'),
155
157
  });
156
158
  }
157
159
  break;
@@ -202,6 +204,7 @@ class WebServerManager {
202
204
  const youtubeUrl = `https://www.youtube.com/watch?v=${this.internalState.currentTrack.videoId}`;
203
205
  void playerService.play(youtubeUrl, {
204
206
  volume: this.internalState.volume,
207
+ volumeFadeDuration: config.get('volumeFadeDuration'),
205
208
  });
206
209
  }
207
210
  break;
@@ -402,6 +402,7 @@ function PlayerManager() {
402
402
  gaplessPlayback: config.get('gaplessPlayback') ?? true,
403
403
  crossfadeDuration: config.get('crossfadeDuration') ?? 0,
404
404
  equalizerPreset: config.get('equalizerPreset') ?? 'flat',
405
+ volumeFadeDuration: config.get('volumeFadeDuration') ?? 0,
405
406
  });
406
407
  logger.info('PlayerManager', 'Playback started successfully', {
407
408
  attempt,
@@ -23,6 +23,7 @@ export interface Config {
23
23
  audioNormalization?: boolean;
24
24
  gaplessPlayback?: boolean;
25
25
  crossfadeDuration?: number;
26
+ volumeFadeDuration?: number;
26
27
  equalizerPreset?: EqualizerPreset;
27
28
  notifications?: boolean;
28
29
  scrobbling?: {
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.44",
3
+ "version": "0.0.45",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",