@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 +28 -0
- package/dist/package.json +120 -0
- package/dist/scripts/build-cli.d.ts +1 -0
- package/dist/scripts/build-cli.js +46 -0
- package/dist/source/cli.js +27 -1
- package/dist/source/components/settings/Settings.js +26 -13
- package/dist/source/services/completions/completions.service.d.ts +2 -0
- package/dist/source/services/completions/completions.service.js +313 -0
- 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/source/utils/constants.d.ts +1 -1
- package/dist/source/utils/constants.js +28 -1
- package/dist/{youtube-music-cli.exe → youtube-music-cli} +0 -0
- package/package.json +9 -7
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));
|
package/dist/source/cli.js
CHANGED
|
@@ -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 === '
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const APP_NAME = "@involvex/youtube-music-cli";
|
|
2
|
-
export declare const APP_VERSION
|
|
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 =
|
|
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`
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@involvex/youtube-music-cli",
|
|
3
|
-
"version": "0.0.
|
|
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
|
|
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",
|