@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 +6 -0
- package/dist/package.json +1 -1
- package/dist/source/cli.js +1 -0
- package/dist/source/components/settings/Settings.js +26 -13
- package/dist/source/services/config/config.service.js +1 -0
- package/dist/source/services/player/player.service.d.ts +1 -0
- package/dist/source/services/player/player.service.js +5 -0
- package/dist/source/services/web/web-server-manager.js +3 -0
- package/dist/source/stores/player.store.js +1 -0
- package/dist/source/types/config.types.d.ts +1 -0
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
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
package/dist/source/cli.js
CHANGED
|
@@ -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
|
-
|
|
155
|
+
cycleVolumeFadeDuration();
|
|
146
156
|
}
|
|
147
157
|
else if (selectedIndex === 5) {
|
|
148
|
-
|
|
158
|
+
cycleEqualizerPreset();
|
|
149
159
|
}
|
|
150
160
|
else if (selectedIndex === 6) {
|
|
151
|
-
|
|
161
|
+
toggleNotifications();
|
|
152
162
|
}
|
|
153
163
|
else if (selectedIndex === 7) {
|
|
154
|
-
|
|
164
|
+
toggleDiscordRpc();
|
|
155
165
|
}
|
|
156
166
|
else if (selectedIndex === 8) {
|
|
157
|
-
|
|
167
|
+
toggleDownloadsEnabled();
|
|
158
168
|
}
|
|
159
169
|
else if (selectedIndex === 9) {
|
|
160
|
-
|
|
170
|
+
setIsEditingDownloadDirectory(true);
|
|
161
171
|
}
|
|
162
172
|
else if (selectedIndex === 10) {
|
|
163
|
-
|
|
173
|
+
cycleDownloadFormat();
|
|
164
174
|
}
|
|
165
175
|
else if (selectedIndex === 11) {
|
|
166
|
-
|
|
176
|
+
cycleSleepTimer();
|
|
167
177
|
}
|
|
168
178
|
else if (selectedIndex === 12) {
|
|
169
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.
|
|
179
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
|
|
170
180
|
}
|
|
171
181
|
else if (selectedIndex === 13) {
|
|
172
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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
|
}
|
|
@@ -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,
|
package/dist/youtube-music-cli
CHANGED
|
Binary file
|