@involvex/youtube-music-cli 0.0.39 → 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,31 @@
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
+
7
+ ## [0.0.44](https://github.com/involvex/youtube-music-cli/compare/v0.0.43...v0.0.44) (2026-02-23)
8
+
9
+ ### Features
10
+
11
+ - add standalone executable support with build-time versioning ([18f730c](https://github.com/involvex/youtube-music-cli/commit/18f730cef46dfe862cfbce0065af4b4989296ef0))
12
+
13
+ ## [0.0.43](https://github.com/involvex/youtube-music-cli/compare/v0.0.42...v0.0.43) (2026-02-23)
14
+
15
+ ## [0.0.42](https://github.com/involvex/youtube-music-cli/compare/v0.0.41...v0.0.42) (2026-02-23)
16
+
17
+ ## [0.0.41](https://github.com/involvex/youtube-music-cli/compare/v0.0.40...v0.0.41) (2026-02-23)
18
+
19
+ ### Features
20
+
21
+ - add shell completions for bash, zsh, powershell, and fish ([5adecc3](https://github.com/involvex/youtube-music-cli/commit/5adecc3af1cf6dae5a188c24c598e67c8260c844))
22
+
23
+ ## [0.0.40](https://github.com/involvex/youtube-music-cli/compare/v0.0.39...v0.0.40) (2026-02-23)
24
+
25
+ ### Features
26
+
27
+ - enable MSIX packaging for Windows distribution ([14eafbf](https://github.com/involvex/youtube-music-cli/commit/14eafbf522f1d2fbacf7ed1243df035cd254891b))
28
+
1
29
  ## [0.0.39](https://github.com/involvex/youtube-music-cli/compare/v0.0.38...v0.0.39) (2026-02-23)
2
30
 
3
31
  ### Features
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@involvex/youtube-music-cli",
3
+ "version": "0.0.45",
4
+ "description": "- A Commandline music player for youtube-music",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/involvex/youtube-music-cli.git"
8
+ },
9
+ "funding": "https://github.com/sponsors/involvex",
10
+ "license": "MIT",
11
+ "author": "involvex",
12
+ "type": "module",
13
+ "bin": {
14
+ "youtube-music-cli": "dist/source/cli.js",
15
+ "ymc": "dist/source/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "CHANGELOG.md",
21
+ "LICENSE",
22
+ ".github/FUNDING.yml"
23
+ ],
24
+ "icon": "assets/icon2.PNG",
25
+ "sponsor": {
26
+ "url": "https://github.com/sponsors/involvex"
27
+ },
28
+ "homepage": "https://involvex.github.io/youtube-music-cli/",
29
+ "keywords": [
30
+ "cli",
31
+ "youtube",
32
+ "youtube-music",
33
+ "youtube-cli",
34
+ "youtube-music-cli",
35
+ "ink",
36
+ "cli-music"
37
+ ],
38
+ "scripts": {
39
+ "prebuild": "bun run format && bun run lint:fix && bun run typecheck",
40
+ "build": "tsc",
41
+ "bun:build": "bun build source/cli.tsx --outfile dist/index.js --target node --footer \"//Copyright (c) 2026 involvex\"",
42
+ "compile": "bun scripts/build-cli.ts",
43
+ "dev": "bun run --bun source/cli.tsx",
44
+ "dev:watch": "bun run --bun --watch source/cli.tsx",
45
+ "format": "prettier --write .",
46
+ "format:check": "prettier --check .",
47
+ "lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern dist",
48
+ "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix --ignore-pattern dist",
49
+ "start": "bun run dist/source/cli.js",
50
+ "test": "bun run build && ava",
51
+ "typecheck": "tsc --noEmit",
52
+ "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
53
+ "clean": "rimraf dist",
54
+ "release": "powershell -File scripts/release.ps1",
55
+ "build:web": "cd web && bun run build",
56
+ "dev:web": "cd web && bun run dev",
57
+ "build:all": "bun run build && bun run build:web",
58
+ "msix": "bun run compile && msix-packager-cli package --config ./msix-config.json --skip-build"
59
+ },
60
+ "prettier": "@vdemedes/prettier-config",
61
+ "ava": {
62
+ "files": [
63
+ "tests/**/*.js"
64
+ ]
65
+ },
66
+ "dependencies": {
67
+ "@distube/ytdl-core": "^4.16.12",
68
+ "@types/bun": "^1.3.9",
69
+ "ansi-escapes": "^7.3.0",
70
+ "discord-rpc": "^4.0.1",
71
+ "ink": "^6.8.0",
72
+ "ink-table": "^3.1.0",
73
+ "ink-text-input": "^6.0.0",
74
+ "jiti": "^2.6.1",
75
+ "meow": "^14.1.0",
76
+ "node-notifier": "^10.0.1",
77
+ "node-youtube-music": "^0.10.3",
78
+ "play-sound": "^1.1.6",
79
+ "react": "^19.2.4",
80
+ "ws": "^8.19.0",
81
+ "youtube-ext": "^1.1.25",
82
+ "youtubei.js": "^16.0.1"
83
+ },
84
+ "devDependencies": {
85
+ "@eslint/js": "^10.0.1",
86
+ "@sindresorhus/tsconfig": "^8.1.0",
87
+ "@types/node": "^25.3.0",
88
+ "@types/node-notifier": "^8.0.5",
89
+ "@types/react": "^19.2.14",
90
+ "@types/ws": "^8.18.1",
91
+ "@vdemedes/prettier-config": "^2.0.1",
92
+ "ava": "^6.4.1",
93
+ "chalk": "^5.6.2",
94
+ "conventional-changelog-cli": "^5.0.0",
95
+ "eslint": "^10.0.1",
96
+ "eslint-plugin-react": "^7.37.5",
97
+ "eslint-plugin-react-hooks": "^7.0.1",
98
+ "globals": "^17.3.0",
99
+ "ink-testing-library": "^4.0.0",
100
+ "prettier": "^3.8.1",
101
+ "prettier-plugin-organize-imports": "^4.3.0",
102
+ "prettier-plugin-packagejson": "^3.0.0",
103
+ "prettier-plugin-sort-imports": "^1.8.11",
104
+ "react-devtools-core": "^7.0.1",
105
+ "rimraf": "^6.1.3",
106
+ "ts-node": "^10.9.2",
107
+ "typescript": "^5.9.3",
108
+ "typescript-eslint": "^8.56.0"
109
+ },
110
+ "engines": {
111
+ "node": ">=16"
112
+ },
113
+ "sponsors": "https://github.com/sponsors/involvex",
114
+ "overrides": {
115
+ "minimatch": "^10.2.1"
116
+ },
117
+ "trustedDependencies": [
118
+ "unrs-resolver"
119
+ ]
120
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import path from 'node:path';
2
+ import pkg from '../package.json' with { type: 'json' };
3
+ const rootDir = process.cwd();
4
+ const isWindows = process.platform === 'win32';
5
+ const outputName = isWindows ? 'youtube-music-cli.exe' : 'youtube-music-cli';
6
+ const outfile = path.join(rootDir, 'dist', outputName);
7
+ const banner = '//Copyright (c) 2026 involvex';
8
+ const iconPath = path.join(rootDir, 'assets', 'icon.ico');
9
+ const platformTarget = isWindows
10
+ ? 'bun-windows-x64'
11
+ : process.platform === 'darwin'
12
+ ? 'bun-darwin-x64'
13
+ : 'bun-linux-x64';
14
+ const compileOptions = isWindows
15
+ ? {
16
+ target: platformTarget,
17
+ outfile,
18
+ footer: banner,
19
+ windowsTitle: 'YouTube Music CLI',
20
+ windowsPublisher: 'involvex',
21
+ windowsIcon: iconPath,
22
+ windowsCopyright: 'Copyright (c) 2026 involvex',
23
+ windowsDescription: 'A Commandline music player for youtube-music',
24
+ }
25
+ : {
26
+ target: platformTarget,
27
+ outfile,
28
+ };
29
+ const buildOptions = {
30
+ entrypoints: [path.join(rootDir, 'source', 'cli.tsx')],
31
+ compile: compileOptions,
32
+ footer: banner,
33
+ minify: true,
34
+ sourcemap: 'linked',
35
+ bytecode: false,
36
+ define: {
37
+ 'process.env.NODE_ENV': JSON.stringify('production'),
38
+ VERSION: JSON.stringify(pkg.version ?? '0.0.0'),
39
+ },
40
+ };
41
+ const result = await Bun.build(buildOptions);
42
+ if (!result.success) {
43
+ console.error('Build failed', result.logs ?? result);
44
+ process.exit(1);
45
+ }
46
+ console.log('Build succeeded:', result.outputs.map(output => output.path));
@@ -10,12 +10,19 @@ import { getImportService } from "./services/import/import.service.js";
10
10
  import { getWebServerManager } from "./services/web/web-server-manager.js";
11
11
  import { getWebStreamingService } from "./services/web/web-streaming.service.js";
12
12
  import { getVersionCheckService } from "./services/version-check/version-check.service.js";
13
+ import { generateCompletion, } from "./services/completions/completions.service.js";
13
14
  import { getConfigService } from "./services/config/config.service.js";
14
15
  import { getPlayerService } from "./services/player/player.service.js";
15
16
  import { APP_VERSION } from "./utils/constants.js";
16
17
  import { ensurePlaybackDependencies } from "./services/player/dependency-check.service.js";
17
18
  import { getMusicService } from "./services/youtube-music/api.js";
19
+ const isStandalone = process
20
+ .isStandaloneExecutable ||
21
+ globalThis.Bun?.isStandalone;
22
+ const argv = isStandalone ? process.argv.slice(1) : process.argv.slice(2);
18
23
  const cli = meow(`
24
+ youtube-music-cli@${APP_VERSION}
25
+
19
26
  Usage
20
27
  $ youtube-music-cli
21
28
  $ youtube-music-cli play <track-id|youtube-url>
@@ -53,6 +60,12 @@ const cli = meow(`
53
60
  --name Custom name for imported playlist
54
61
  --help, -h Show this help
55
62
 
63
+ Shell Completions
64
+ $ youtube-music-cli completions bash
65
+ $ youtube-music-cli completions zsh
66
+ $ youtube-music-cli completions powershell
67
+ $ youtube-music-cli completions fish
68
+
56
69
  Examples
57
70
  $ youtube-music-cli
58
71
  $ youtube-music-cli play dQw4w9WgXcQ
@@ -61,8 +74,10 @@ const cli = meow(`
61
74
  $ youtube-music-cli plugins install adblock
62
75
  $ youtube-music-cli import spotify "https://open.spotify.com/playlist/..."
63
76
  $ youtube-music-cli --web --web-port 3000
77
+ $ youtube-music-cli completions powershell | Out-File $PROFILE
64
78
  `, {
65
79
  importMeta: import.meta,
80
+ argv,
66
81
  flags: {
67
82
  theme: {
68
83
  type: 'string',
@@ -142,6 +157,7 @@ async function runDirectPlaybackCommand(flags) {
142
157
  const playbackOptions = {
143
158
  volume: flags.volume ?? config.get('volume'),
144
159
  audioNormalization: config.get('audioNormalization'),
160
+ volumeFadeDuration: config.get('volumeFadeDuration'),
145
161
  };
146
162
  let track;
147
163
  if (flags.playTrack) {
@@ -307,7 +323,17 @@ if (command === 'plugins') {
307
323
  }
308
324
  else {
309
325
  // Handle other direct commands
310
- if (command === 'play' && args[0]) {
326
+ if (command === 'completions') {
327
+ const shell = args[0];
328
+ const validShells = ['bash', 'zsh', 'powershell', 'fish'];
329
+ if (!shell || !validShells.includes(shell)) {
330
+ console.error('Usage: youtube-music-cli completions <bash|zsh|powershell|fish>');
331
+ process.exit(1);
332
+ }
333
+ console.log(generateCompletion(shell));
334
+ process.exit(0);
335
+ }
336
+ else if (command === 'play' && args[0]) {
311
337
  // Play specific track
312
338
  cli.flags.playTrack = args[0];
313
339
  }
@@ -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
  }
@@ -0,0 +1,2 @@
1
+ export type ShellType = 'bash' | 'zsh' | 'powershell' | 'fish';
2
+ export declare function generateCompletion(shell: ShellType): string;
@@ -0,0 +1,313 @@
1
+ const COMMANDS = [
2
+ 'play',
3
+ 'search',
4
+ 'playlist',
5
+ 'suggestions',
6
+ 'pause',
7
+ 'resume',
8
+ 'skip',
9
+ 'back',
10
+ 'plugins',
11
+ 'import',
12
+ 'completions',
13
+ ];
14
+ const PLUGINS_SUBCOMMANDS = [
15
+ 'list',
16
+ 'install',
17
+ 'remove',
18
+ 'uninstall',
19
+ 'update',
20
+ 'enable',
21
+ 'disable',
22
+ ];
23
+ const IMPORT_SUBCOMMANDS = ['spotify', 'youtube'];
24
+ const COMPLETIONS_SUBCOMMANDS = [
25
+ 'bash',
26
+ 'zsh',
27
+ 'powershell',
28
+ 'fish',
29
+ ];
30
+ const FLAGS = [
31
+ '--theme',
32
+ '--volume',
33
+ '--shuffle',
34
+ '--repeat',
35
+ '--headless',
36
+ '--web',
37
+ '--web-host',
38
+ '--web-port',
39
+ '--web-only',
40
+ '--web-auth',
41
+ '--name',
42
+ '--help',
43
+ '--version',
44
+ ];
45
+ export function generateCompletion(shell) {
46
+ switch (shell) {
47
+ case 'bash':
48
+ return generateBashCompletion();
49
+ case 'zsh':
50
+ return generateZshCompletion();
51
+ case 'powershell':
52
+ return generatePowerShellCompletion();
53
+ case 'fish':
54
+ return generateFishCompletion();
55
+ }
56
+ }
57
+ function generateBashCompletion() {
58
+ const cmds = COMMANDS.join(' ');
59
+ const pluginsSubs = PLUGINS_SUBCOMMANDS.join(' ');
60
+ const importSubs = IMPORT_SUBCOMMANDS.join(' ');
61
+ const completionsSubs = COMPLETIONS_SUBCOMMANDS.join(' ');
62
+ const flags = FLAGS.join(' ');
63
+ return `# youtube-music-cli bash completion
64
+ # Add to ~/.bashrc or ~/.bash_profile:
65
+ # source <(ymc completions bash)
66
+ # # or:
67
+ # ymc completions bash >> ~/.bash_completion
68
+
69
+ _ymc_completions() {
70
+ local cur prev words cword
71
+ _init_completion || return
72
+
73
+ local commands="${cmds}"
74
+ local flags="${flags}"
75
+
76
+ case "$prev" in
77
+ plugins)
78
+ COMPREPLY=($(compgen -W "${pluginsSubs}" -- "$cur"))
79
+ return ;;
80
+ import)
81
+ COMPREPLY=($(compgen -W "${importSubs}" -- "$cur"))
82
+ return ;;
83
+ completions)
84
+ COMPREPLY=($(compgen -W "${completionsSubs}" -- "$cur"))
85
+ return ;;
86
+ --theme|-t)
87
+ COMPREPLY=($(compgen -W "dark light midnight matrix" -- "$cur"))
88
+ return ;;
89
+ --repeat|-r)
90
+ COMPREPLY=($(compgen -W "off all one" -- "$cur"))
91
+ return ;;
92
+ esac
93
+
94
+ if [[ "$cur" == -* ]]; then
95
+ COMPREPLY=($(compgen -W "$flags" -- "$cur"))
96
+ else
97
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
98
+ fi
99
+ }
100
+
101
+ complete -F _ymc_completions ymc youtube-music-cli
102
+ `;
103
+ }
104
+ function generateZshCompletion() {
105
+ return `#compdef ymc youtube-music-cli
106
+ # youtube-music-cli zsh completion
107
+ # Add to your zsh config:
108
+ # source <(ymc completions zsh)
109
+ # # or copy to a directory in $fpath:
110
+ # ymc completions zsh > ~/.zsh/completions/_ymc
111
+
112
+ _ymc() {
113
+ local -a commands subcommands flags
114
+
115
+ commands=(
116
+ 'play:Play a track by ID or YouTube URL'
117
+ 'search:Search for tracks'
118
+ 'playlist:Play a playlist by ID'
119
+ 'suggestions:Show music suggestions'
120
+ 'pause:Pause playback'
121
+ 'resume:Resume playback'
122
+ 'skip:Skip to next track'
123
+ 'back:Go to previous track'
124
+ 'plugins:Manage plugins'
125
+ 'import:Import playlists from Spotify or YouTube'
126
+ 'completions:Generate shell completion scripts'
127
+ )
128
+
129
+ flags=(
130
+ '--theme[Theme to use]:theme:(dark light midnight matrix)'
131
+ '--volume[Initial volume (0-100)]:volume:'
132
+ '--shuffle[Enable shuffle mode]'
133
+ '--repeat[Repeat mode]:mode:(off all one)'
134
+ '--headless[Run without TUI]'
135
+ '--web[Enable web UI server]'
136
+ '--web-host[Web server host]:host:'
137
+ '--web-port[Web server port]:port:'
138
+ '--web-only[Run web server without CLI UI]'
139
+ '--web-auth[Authentication token for web server]:token:'
140
+ '--name[Custom name for imported playlist]:name:'
141
+ '--help[Show help]'
142
+ '--version[Show version]'
143
+ )
144
+
145
+ case $words[2] in
146
+ plugins)
147
+ local -a plugin_cmds
148
+ plugin_cmds=(
149
+ 'list:List installed plugins'
150
+ 'install:Install a plugin'
151
+ 'remove:Remove a plugin'
152
+ 'uninstall:Alias for remove'
153
+ 'update:Update a plugin'
154
+ 'enable:Enable a plugin'
155
+ 'disable:Disable a plugin'
156
+ )
157
+ _describe 'plugin commands' plugin_cmds
158
+ return ;;
159
+ import)
160
+ local -a import_sources
161
+ import_sources=('spotify:Import from Spotify' 'youtube:Import from YouTube')
162
+ _describe 'import sources' import_sources
163
+ return ;;
164
+ completions)
165
+ local -a shells
166
+ shells=('bash:Bash completion' 'zsh:Zsh completion' 'powershell:PowerShell completion' 'fish:Fish completion')
167
+ _describe 'shells' shells
168
+ return ;;
169
+ esac
170
+
171
+ _arguments -s \\
172
+ $flags \\
173
+ '1:command:->cmd' \\
174
+ '*::args:->args'
175
+
176
+ case $state in
177
+ cmd)
178
+ _describe 'commands' commands ;;
179
+ args)
180
+ _message 'arguments' ;;
181
+ esac
182
+ }
183
+
184
+ _ymc
185
+ `;
186
+ }
187
+ function generatePowerShellCompletion() {
188
+ const cmds = COMMANDS.map(c => `'${c}'`).join(', ');
189
+ const pluginsSubs = PLUGINS_SUBCOMMANDS.map(c => `'${c}'`).join(', ');
190
+ const importSubs = IMPORT_SUBCOMMANDS.map(c => `'${c}'`).join(', ');
191
+ const completionsSubs = COMPLETIONS_SUBCOMMANDS.map(c => `'${c}'`).join(', ');
192
+ const flags = FLAGS.map(f => `'${f}'`).join(', ');
193
+ return `# youtube-music-cli PowerShell completion
194
+ # Add to your PowerShell profile ($PROFILE):
195
+ # ymc completions powershell | Out-File -Append $PROFILE
196
+ # # or:
197
+ # Invoke-Expression (ymc completions powershell | Out-String)
198
+
199
+ $ymcCompleterBlock = {
200
+ param($wordToComplete, $commandAst, $cursorPosition)
201
+
202
+ $commands = @(${cmds})
203
+ $pluginsSubCommands = @(${pluginsSubs})
204
+ $importSubCommands = @(${importSubs})
205
+ $completionsSubCommands = @(${completionsSubs})
206
+ $flags = @(${flags})
207
+ $themes = @('dark', 'light', 'midnight', 'matrix')
208
+ $repeatModes = @('off', 'all', 'one')
209
+
210
+ $tokens = $commandAst.CommandElements
211
+ $prevToken = if ($tokens.Count -ge 2) { $tokens[$tokens.Count - 2].ToString() } else { '' }
212
+ $firstArg = if ($tokens.Count -ge 2) { $tokens[1].ToString() } else { '' }
213
+
214
+ # Context-aware completions
215
+ switch ($prevToken) {
216
+ 'plugins' {
217
+ $pluginsSubCommands | Where-Object { $_ -like "$wordToComplete*" } |
218
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
219
+ return
220
+ }
221
+ 'import' {
222
+ $importSubCommands | Where-Object { $_ -like "$wordToComplete*" } |
223
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
224
+ return
225
+ }
226
+ 'completions' {
227
+ $completionsSubCommands | Where-Object { $_ -like "$wordToComplete*" } |
228
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
229
+ return
230
+ }
231
+ { $_ -in '--theme', '-t' } {
232
+ $themes | Where-Object { $_ -like "$wordToComplete*" } |
233
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
234
+ return
235
+ }
236
+ { $_ -in '--repeat', '-r' } {
237
+ $repeatModes | Where-Object { $_ -like "$wordToComplete*" } |
238
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
239
+ return
240
+ }
241
+ }
242
+
243
+ if ($wordToComplete.StartsWith('-')) {
244
+ $flags | Where-Object { $_ -like "$wordToComplete*" } |
245
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
246
+ } elseif ($firstArg -eq $wordToComplete -or $tokens.Count -le 1) {
247
+ $commands | Where-Object { $_ -like "$wordToComplete*" } |
248
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
249
+ }
250
+ }
251
+
252
+ Register-ArgumentCompleter -Native -CommandName @('ymc', 'youtube-music-cli') -ScriptBlock $ymcCompleterBlock
253
+ `;
254
+ }
255
+ function generateFishCompletion() {
256
+ const commandCompletions = COMMANDS.map(cmd => `complete -c ymc -n '__fish_use_subcommand' -f -a '${cmd}' -d '${getFishDescription(cmd)}'`).join('\n');
257
+ const pluginsCompletions = PLUGINS_SUBCOMMANDS.map(sub => `complete -c ymc -n '__fish_seen_subcommand_from plugins' -f -a '${sub}'`).join('\n');
258
+ const importCompletions = IMPORT_SUBCOMMANDS.map(sub => `complete -c ymc -n '__fish_seen_subcommand_from import' -f -a '${sub}'`).join('\n');
259
+ const completionsCompletions = COMPLETIONS_SUBCOMMANDS.map(sub => `complete -c ymc -n '__fish_seen_subcommand_from completions' -f -a '${sub}'`).join('\n');
260
+ return `# youtube-music-cli fish completion
261
+ # Save to: ~/.config/fish/completions/ymc.fish
262
+ # ymc completions fish > ~/.config/fish/completions/ymc.fish
263
+
264
+ # Disable file completions by default
265
+ complete -c ymc -f
266
+
267
+ # Main commands
268
+ ${commandCompletions}
269
+
270
+ # Plugins subcommands
271
+ ${pluginsCompletions}
272
+
273
+ # Import subcommands
274
+ ${importCompletions}
275
+
276
+ # Completions subcommands
277
+ ${completionsCompletions}
278
+
279
+ # Flags
280
+ complete -c ymc -l theme -s t -d 'Theme to use' -r -a 'dark light midnight matrix'
281
+ complete -c ymc -l volume -s v -d 'Initial volume (0-100)' -r
282
+ complete -c ymc -l shuffle -s s -d 'Enable shuffle mode'
283
+ complete -c ymc -l repeat -s r -d 'Repeat mode' -r -a 'off all one'
284
+ complete -c ymc -l headless -d 'Run without TUI'
285
+ complete -c ymc -l web -d 'Enable web UI server'
286
+ complete -c ymc -l web-host -d 'Web server host' -r
287
+ complete -c ymc -l web-port -d 'Web server port' -r
288
+ complete -c ymc -l web-only -d 'Run web server without CLI UI'
289
+ complete -c ymc -l web-auth -d 'Authentication token for web server' -r
290
+ complete -c ymc -l name -d 'Custom name for imported playlist' -r
291
+ complete -c ymc -l help -s h -d 'Show help'
292
+ complete -c ymc -l version -d 'Show version'
293
+
294
+ # Also register for youtube-music-cli
295
+ complete -c youtube-music-cli -w ymc
296
+ `;
297
+ }
298
+ function getFishDescription(cmd) {
299
+ const descriptions = {
300
+ play: 'Play a track by ID or YouTube URL',
301
+ search: 'Search for tracks',
302
+ playlist: 'Play a playlist by ID',
303
+ suggestions: 'Show music suggestions',
304
+ pause: 'Pause playback',
305
+ resume: 'Resume playback',
306
+ skip: 'Skip to next track',
307
+ back: 'Go to previous track',
308
+ plugins: 'Manage plugins',
309
+ import: 'Import playlists from Spotify or YouTube',
310
+ completions: 'Generate shell completion scripts',
311
+ };
312
+ return descriptions[cmd] ?? cmd;
313
+ }
@@ -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?: {
@@ -1,5 +1,5 @@
1
1
  export declare const APP_NAME = "@involvex/youtube-music-cli";
2
- export declare const APP_VERSION = "0.0.20";
2
+ export declare const APP_VERSION: string;
3
3
  export declare const CONFIG_DIR: string;
4
4
  export declare const CONFIG_FILE: string;
5
5
  export declare const VIEW: {
@@ -1,6 +1,33 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, resolve } from 'node:path';
4
+ function loadAppVersion() {
5
+ if (typeof VERSION !== 'undefined') {
6
+ return VERSION;
7
+ }
8
+ let dir = dirname(fileURLToPath(import.meta.url));
9
+ for (let i = 0; i < 5; i++) {
10
+ try {
11
+ const content = readFileSync(resolve(dir, 'package.json'), 'utf8');
12
+ const pkg = JSON.parse(content);
13
+ if (typeof pkg.version === 'string' &&
14
+ pkg.name?.includes('youtube-music-cli')) {
15
+ return pkg.version;
16
+ }
17
+ }
18
+ catch {
19
+ /* ignore */
20
+ }
21
+ const parent = dirname(dir);
22
+ if (parent === dir)
23
+ break;
24
+ dir = parent;
25
+ }
26
+ return '0.0.0';
27
+ }
1
28
  // Application constants
2
29
  export const APP_NAME = '@involvex/youtube-music-cli';
3
- export const APP_VERSION = '0.0.20';
30
+ export const APP_VERSION = loadAppVersion();
4
31
  // Config directory
5
32
  export const CONFIG_DIR = process.platform === 'win32'
6
33
  ? `${process.env['USERPROFILE']}\\.youtube-music-cli`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.39",
3
+ "version": "0.0.45",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,8 +38,8 @@
38
38
  "scripts": {
39
39
  "prebuild": "bun run format && bun run lint:fix && bun run typecheck",
40
40
  "build": "tsc",
41
- "bun:build": "bun build source/cli.tsx --outfile dist/index.js --target node",
42
- "compile": "bun build --compile source/cli.tsx --outfile dist/youtube-music-cli.exe",
41
+ "bun:build": "bun build source/cli.tsx --outfile dist/index.js --target node --footer \"//Copyright (c) 2026 involvex\"",
42
+ "compile": "bun scripts/build-cli.ts",
43
43
  "dev": "bun run --bun source/cli.tsx",
44
44
  "dev:watch": "bun run --bun --watch source/cli.tsx",
45
45
  "format": "prettier --write .",
@@ -54,7 +54,8 @@
54
54
  "release": "powershell -File scripts/release.ps1",
55
55
  "build:web": "cd web && bun run build",
56
56
  "dev:web": "cd web && bun run dev",
57
- "build:all": "bun run build && bun run build:web"
57
+ "build:all": "bun run build && bun run build:web",
58
+ "msix": "bun run compile && msix-packager-cli package --config ./msix-config.json --skip-build"
58
59
  },
59
60
  "prettier": "@vdemedes/prettier-config",
60
61
  "ava": {
@@ -64,20 +65,21 @@
64
65
  },
65
66
  "dependencies": {
66
67
  "@distube/ytdl-core": "^4.16.12",
68
+ "@types/bun": "^1.3.9",
67
69
  "ansi-escapes": "^7.3.0",
70
+ "discord-rpc": "^4.0.1",
68
71
  "ink": "^6.8.0",
69
72
  "ink-table": "^3.1.0",
70
73
  "ink-text-input": "^6.0.0",
71
74
  "jiti": "^2.6.1",
72
75
  "meow": "^14.1.0",
73
76
  "node-notifier": "^10.0.1",
74
- "discord-rpc": "^4.0.1",
75
77
  "node-youtube-music": "^0.10.3",
76
78
  "play-sound": "^1.1.6",
77
79
  "react": "^19.2.4",
80
+ "ws": "^8.19.0",
78
81
  "youtube-ext": "^1.1.25",
79
- "youtubei.js": "^16.0.1",
80
- "ws": "^8.19.0"
82
+ "youtubei.js": "^16.0.1"
81
83
  },
82
84
  "devDependencies": {
83
85
  "@eslint/js": "^10.0.1",