@involvex/youtube-music-cli 0.0.38 → 0.0.44
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 +26 -1
- package/dist/source/components/common/ShortcutsBar.js +57 -11
- 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/player/player.service.js +1 -0
- package/dist/source/services/web/web-server-manager.js +1 -0
- package/dist/source/stores/player.store.js +22 -3
- package/dist/source/types/player.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.44](https://github.com/involvex/youtube-music-cli/compare/v0.0.43...v0.0.44) (2026-02-23)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add standalone executable support with build-time versioning ([18f730c](https://github.com/involvex/youtube-music-cli/commit/18f730cef46dfe862cfbce0065af4b4989296ef0))
|
|
6
|
+
|
|
7
|
+
## [0.0.43](https://github.com/involvex/youtube-music-cli/compare/v0.0.42...v0.0.43) (2026-02-23)
|
|
8
|
+
|
|
9
|
+
## [0.0.42](https://github.com/involvex/youtube-music-cli/compare/v0.0.41...v0.0.42) (2026-02-23)
|
|
10
|
+
|
|
11
|
+
## [0.0.41](https://github.com/involvex/youtube-music-cli/compare/v0.0.40...v0.0.41) (2026-02-23)
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- add shell completions for bash, zsh, powershell, and fish ([5adecc3](https://github.com/involvex/youtube-music-cli/commit/5adecc3af1cf6dae5a188c24c598e67c8260c844))
|
|
16
|
+
|
|
17
|
+
## [0.0.40](https://github.com/involvex/youtube-music-cli/compare/v0.0.39...v0.0.40) (2026-02-23)
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
- enable MSIX packaging for Windows distribution ([14eafbf](https://github.com/involvex/youtube-music-cli/commit/14eafbf522f1d2fbacf7ed1243df035cd254891b))
|
|
22
|
+
|
|
23
|
+
## [0.0.39](https://github.com/involvex/youtube-music-cli/compare/v0.0.38...v0.0.39) (2026-02-23)
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- add visual flash feedback when shortcuts are pressed ([dc3efa8](https://github.com/involvex/youtube-music-cli/commit/dc3efa8f642619ad2e9ce937528e76f4d388e4dc))
|
|
28
|
+
|
|
1
29
|
## [0.0.38](https://github.com/involvex/youtube-music-cli/compare/v0.0.36...v0.0.38) (2026-02-22)
|
|
2
30
|
|
|
3
31
|
### Bug Fixes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@involvex/youtube-music-cli",
|
|
3
|
+
"version": "0.0.44",
|
|
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',
|
|
@@ -307,7 +322,17 @@ if (command === 'plugins') {
|
|
|
307
322
|
}
|
|
308
323
|
else {
|
|
309
324
|
// Handle other direct commands
|
|
310
|
-
if (command === '
|
|
325
|
+
if (command === 'completions') {
|
|
326
|
+
const shell = args[0];
|
|
327
|
+
const validShells = ['bash', 'zsh', 'powershell', 'fish'];
|
|
328
|
+
if (!shell || !validShells.includes(shell)) {
|
|
329
|
+
console.error('Usage: youtube-music-cli completions <bash|zsh|powershell|fish>');
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
console.log(generateCompletion(shell));
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
else if (command === 'play' && args[0]) {
|
|
311
336
|
// Play specific track
|
|
312
337
|
cli.flags.playTrack = args[0];
|
|
313
338
|
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// Shortcuts bar component
|
|
3
|
+
import { useState } from 'react';
|
|
3
4
|
import { Box, Text } from 'ink';
|
|
4
5
|
import { usePlayer } from "../../hooks/usePlayer.js";
|
|
5
6
|
import { useTheme } from "../../hooks/useTheme.js";
|
|
6
7
|
import { useKeyBinding } from "../../hooks/useKeyboard.js";
|
|
7
8
|
import { KEYBINDINGS } from "../../utils/constants.js";
|
|
8
9
|
import { ICONS } from "../../utils/icons.js";
|
|
10
|
+
const FLASH_DURATION_MS = 300;
|
|
9
11
|
export default function ShortcutsBar() {
|
|
10
12
|
const { theme } = useTheme();
|
|
11
13
|
const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, } = usePlayer();
|
|
14
|
+
const [flashState, setFlashState] = useState({});
|
|
15
|
+
const flash = (key) => {
|
|
16
|
+
setFlashState(prev => ({ ...prev, [key]: true }));
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
setFlashState(prev => ({ ...prev, [key]: false }));
|
|
19
|
+
}, FLASH_DURATION_MS);
|
|
20
|
+
};
|
|
21
|
+
const shortcutColor = (key) => flashState[key] ? theme.colors.success : theme.colors.text;
|
|
12
22
|
// Register key bindings globally
|
|
13
23
|
const handlePlayPause = () => {
|
|
24
|
+
flash('playPause');
|
|
14
25
|
if (playerState.isPlaying) {
|
|
15
26
|
pause();
|
|
16
27
|
}
|
|
@@ -19,16 +30,51 @@ export default function ShortcutsBar() {
|
|
|
19
30
|
}
|
|
20
31
|
};
|
|
21
32
|
useKeyBinding(KEYBINDINGS.PLAY_PAUSE, handlePlayPause);
|
|
22
|
-
useKeyBinding(KEYBINDINGS.NEXT,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
useKeyBinding(KEYBINDINGS.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
useKeyBinding(KEYBINDINGS.NEXT, () => {
|
|
34
|
+
flash('next');
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
useKeyBinding(KEYBINDINGS.PREVIOUS, () => {
|
|
38
|
+
flash('prev');
|
|
39
|
+
previous();
|
|
40
|
+
});
|
|
41
|
+
useKeyBinding(KEYBINDINGS.VOLUME_UP, () => {
|
|
42
|
+
flash('volume');
|
|
43
|
+
volumeUp();
|
|
44
|
+
});
|
|
45
|
+
useKeyBinding(KEYBINDINGS.VOLUME_DOWN, () => {
|
|
46
|
+
flash('volume');
|
|
47
|
+
volumeDown();
|
|
48
|
+
});
|
|
49
|
+
useKeyBinding(KEYBINDINGS.VOLUME_FINE_UP, () => {
|
|
50
|
+
flash('volume');
|
|
51
|
+
volumeFineUp();
|
|
52
|
+
});
|
|
53
|
+
useKeyBinding(KEYBINDINGS.VOLUME_FINE_DOWN, () => {
|
|
54
|
+
flash('volume');
|
|
55
|
+
volumeFineDown();
|
|
56
|
+
});
|
|
57
|
+
useKeyBinding(KEYBINDINGS.SHUFFLE, () => {
|
|
58
|
+
flash('shuffle');
|
|
59
|
+
toggleShuffle();
|
|
60
|
+
});
|
|
61
|
+
useKeyBinding(KEYBINDINGS.REPEAT, () => {
|
|
62
|
+
flash('repeat');
|
|
63
|
+
toggleRepeat();
|
|
64
|
+
});
|
|
30
65
|
// Note: SETTINGS keybinding handled by MainLayout to avoid double-dispatch
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
const shuffleColor = flashState['shuffle']
|
|
67
|
+
? theme.colors.success
|
|
68
|
+
: playerState.shuffle
|
|
69
|
+
? theme.colors.primary
|
|
70
|
+
: theme.colors.dim;
|
|
71
|
+
const repeatColor = flashState['repeat']
|
|
72
|
+
? theme.colors.success
|
|
73
|
+
: playerState.repeat !== 'off'
|
|
74
|
+
? theme.colors.secondary
|
|
75
|
+
: theme.colors.dim;
|
|
76
|
+
const volumeColor = flashState['volume']
|
|
77
|
+
? theme.colors.success
|
|
78
|
+
: theme.colors.primary;
|
|
79
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsxs(Text, { color: shortcutColor('playPause'), children: [playerState.isPlaying ? ICONS.PAUSE : ICONS.PLAY_PAUSE_ON, " [Space]"] }), ' ', "| ", _jsxs(Text, { color: shortcutColor('prev'), children: [ICONS.PREV, " [B/\u2190]"] }), " |", ' ', _jsxs(Text, { color: shortcutColor('next'), children: [ICONS.NEXT, " [N/\u2192]"] }), " |", ' ', _jsxs(Text, { color: shuffleColor, children: [ICONS.SHUFFLE, " [Shift+S]"] }), " |", ' ', _jsxs(Text, { color: repeatColor, children: [playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL, ' ', "[R]"] }), ' ', "| ", _jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAYLIST, " [Shift+P]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.DOWNLOAD, " [Shift+D]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SEARCH, " [/]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.HELP, " [?]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.BG_PLAY, " [Shift+Q]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.RESUME, " [Shift+R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.QUIT, " [Q]"] })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: shuffleColor, children: ICONS.SHUFFLE }), ' ', _jsx(Text, { color: repeatColor, children: playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL }), ' ', _jsxs(Text, { color: theme.colors.dim, children: [ICONS.VOLUME, " [+/-]"] }), ' ', _jsxs(Text, { color: volumeColor, children: [playerState.volume, "%"] })] })] }));
|
|
34
80
|
}
|
|
@@ -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
|
+
}
|
|
@@ -23,6 +23,7 @@ const initialState = {
|
|
|
23
23
|
shuffle: false,
|
|
24
24
|
isLoading: false,
|
|
25
25
|
error: null,
|
|
26
|
+
playRequestId: 0,
|
|
26
27
|
};
|
|
27
28
|
// Get player service instance
|
|
28
29
|
const playerService = getPlayerService();
|
|
@@ -35,6 +36,7 @@ export function playerReducer(state, action) {
|
|
|
35
36
|
isPlaying: true,
|
|
36
37
|
progress: 0,
|
|
37
38
|
error: null,
|
|
39
|
+
playRequestId: state.playRequestId + 1,
|
|
38
40
|
};
|
|
39
41
|
case 'PAUSE':
|
|
40
42
|
return { ...state, isPlaying: false };
|
|
@@ -62,6 +64,7 @@ export function playerReducer(state, action) {
|
|
|
62
64
|
currentTrack: state.queue[randomIndex] ?? null,
|
|
63
65
|
isPlaying: true,
|
|
64
66
|
progress: 0,
|
|
67
|
+
playRequestId: state.playRequestId + 1,
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
// Sequential mode
|
|
@@ -74,6 +77,7 @@ export function playerReducer(state, action) {
|
|
|
74
77
|
currentTrack: state.queue[0] ?? null,
|
|
75
78
|
isPlaying: true,
|
|
76
79
|
progress: 0,
|
|
80
|
+
playRequestId: state.playRequestId + 1,
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
return state;
|
|
@@ -84,6 +88,7 @@ export function playerReducer(state, action) {
|
|
|
84
88
|
currentTrack: state.queue[nextPosition] ?? null,
|
|
85
89
|
isPlaying: true,
|
|
86
90
|
progress: 0,
|
|
91
|
+
playRequestId: state.playRequestId + 1,
|
|
87
92
|
};
|
|
88
93
|
}
|
|
89
94
|
case 'PREVIOUS':
|
|
@@ -95,6 +100,7 @@ export function playerReducer(state, action) {
|
|
|
95
100
|
return {
|
|
96
101
|
...state,
|
|
97
102
|
progress: 0,
|
|
103
|
+
playRequestId: state.playRequestId + 1,
|
|
98
104
|
};
|
|
99
105
|
}
|
|
100
106
|
return {
|
|
@@ -102,6 +108,7 @@ export function playerReducer(state, action) {
|
|
|
102
108
|
queuePosition: prevPosition,
|
|
103
109
|
currentTrack: state.queue[prevPosition] ?? null,
|
|
104
110
|
progress: 0,
|
|
111
|
+
playRequestId: state.playRequestId + 1,
|
|
105
112
|
};
|
|
106
113
|
case 'SEEK':
|
|
107
114
|
return {
|
|
@@ -174,6 +181,7 @@ export function playerReducer(state, action) {
|
|
|
174
181
|
queuePosition: action.position,
|
|
175
182
|
currentTrack: state.queue[action.position] ?? null,
|
|
176
183
|
progress: 0,
|
|
184
|
+
playRequestId: state.playRequestId + 1,
|
|
177
185
|
};
|
|
178
186
|
}
|
|
179
187
|
return state;
|
|
@@ -207,6 +215,7 @@ export function playerReducer(state, action) {
|
|
|
207
215
|
hasTrack: !!action.currentTrack,
|
|
208
216
|
queueLength: action.queue.length,
|
|
209
217
|
});
|
|
218
|
+
playerService.setVolume(action.volume);
|
|
210
219
|
return {
|
|
211
220
|
...state,
|
|
212
221
|
currentTrack: action.currentTrack,
|
|
@@ -292,6 +301,7 @@ function PlayerManager() {
|
|
|
292
301
|
};
|
|
293
302
|
}, [dispatch, playerService]);
|
|
294
303
|
// Handle track changes
|
|
304
|
+
const lastPlayedRequestId = useRef(-1);
|
|
295
305
|
useEffect(() => {
|
|
296
306
|
const track = state.currentTrack;
|
|
297
307
|
if (!track) {
|
|
@@ -306,14 +316,17 @@ function PlayerManager() {
|
|
|
306
316
|
});
|
|
307
317
|
return;
|
|
308
318
|
}
|
|
309
|
-
// Guard:
|
|
319
|
+
// Guard: Don't replay same track unless a new play request was explicitly dispatched
|
|
310
320
|
const currentTrackId = playerService.getCurrentTrackId?.() || '';
|
|
311
|
-
|
|
321
|
+
const isSameTrack = currentTrackId === track.videoId;
|
|
322
|
+
const isNewPlayRequest = state.playRequestId !== lastPlayedRequestId.current;
|
|
323
|
+
if (isSameTrack && !isNewPlayRequest) {
|
|
312
324
|
logger.debug('PlayerManager', 'Track already playing, skipping', {
|
|
313
325
|
videoId: track.videoId,
|
|
314
326
|
});
|
|
315
327
|
return;
|
|
316
328
|
}
|
|
329
|
+
lastPlayedRequestId.current = state.playRequestId;
|
|
317
330
|
logger.info('PlayerManager', 'Loading track', {
|
|
318
331
|
title: track.title,
|
|
319
332
|
videoId: track.videoId,
|
|
@@ -424,7 +437,13 @@ function PlayerManager() {
|
|
|
424
437
|
void loadAndPlayTrack();
|
|
425
438
|
// Note: state.volume intentionally excluded - volume changes should not restart playback
|
|
426
439
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
427
|
-
}, [
|
|
440
|
+
}, [
|
|
441
|
+
state.currentTrack,
|
|
442
|
+
state.isPlaying,
|
|
443
|
+
state.playRequestId,
|
|
444
|
+
dispatch,
|
|
445
|
+
musicService,
|
|
446
|
+
]);
|
|
428
447
|
// Handle progress tracking
|
|
429
448
|
useEffect(() => {
|
|
430
449
|
if (state.isPlaying && state.currentTrack) {
|
|
@@ -13,5 +13,6 @@ export interface PlayerState {
|
|
|
13
13
|
shuffle: boolean;
|
|
14
14
|
isLoading: boolean;
|
|
15
15
|
error: string | null;
|
|
16
|
+
playRequestId: number;
|
|
16
17
|
}
|
|
17
18
|
export type PlayerAction = PlayAction | PauseAction | ResumeAction | StopAction | NextAction | PreviousAction | SeekAction | SetVolumeAction | VolumeUpAction | VolumeDownAction | VolumeFineUpAction | VolumeFineDownAction | ToggleShuffleAction | ToggleRepeatAction | SetQueueAction | AddToQueueAction | RemoveFromQueueAction | ClearQueueAction | SetQueuePositionAction | UpdateProgressAction | SetDurationAction | TickAction | SetLoadingAction | SetErrorAction | RestoreStateAction | SetSpeedAction;
|
|
@@ -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.44",
|
|
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",
|