@involvex/youtube-music-cli 0.0.47 → 0.0.48

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/cli.js.map +3 -3
  3. package/dist/youtube-music-cli +0 -0
  4. package/package.json +1 -1
  5. package/dist/eslint.config.js +0 -55
  6. package/dist/package.json +0 -120
  7. package/dist/scripts/build-cli.js +0 -46
  8. package/dist/source/app.js +0 -17
  9. package/dist/source/cli.js +0 -504
  10. package/dist/source/components/common/ErrorBoundary.js +0 -22
  11. package/dist/source/components/common/Help.js +0 -18
  12. package/dist/source/components/common/ShortcutsBar.js +0 -89
  13. package/dist/source/components/config/ConfigLayout.js +0 -84
  14. package/dist/source/components/config/KeybindingsLayout.js +0 -107
  15. package/dist/source/components/export/ExportLayout.js +0 -111
  16. package/dist/source/components/import/ImportLayout.js +0 -119
  17. package/dist/source/components/import/ImportProgress.js +0 -73
  18. package/dist/source/components/layouts/ExploreLayout.js +0 -72
  19. package/dist/source/components/layouts/HistoryLayout.js +0 -37
  20. package/dist/source/components/layouts/LyricsLayout.js +0 -89
  21. package/dist/source/components/layouts/MainLayout.js +0 -190
  22. package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
  23. package/dist/source/components/layouts/PlayerLayout.js +0 -9
  24. package/dist/source/components/layouts/PluginsLayout.js +0 -77
  25. package/dist/source/components/layouts/SearchLayout.js +0 -193
  26. package/dist/source/components/layouts/TrendingLayout.js +0 -59
  27. package/dist/source/components/player/NowPlaying.js +0 -45
  28. package/dist/source/components/player/PlayerControls.js +0 -83
  29. package/dist/source/components/player/ProgressBar.js +0 -19
  30. package/dist/source/components/player/QueueList.js +0 -36
  31. package/dist/source/components/player/Suggestions.js +0 -50
  32. package/dist/source/components/playlist/PlaylistList.js +0 -138
  33. package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
  34. package/dist/source/components/plugins/PluginsAvailable.js +0 -55
  35. package/dist/source/components/plugins/PluginsList.js +0 -18
  36. package/dist/source/components/search/SearchBar.js +0 -55
  37. package/dist/source/components/search/SearchHistory.js +0 -35
  38. package/dist/source/components/search/SearchResults.js +0 -280
  39. package/dist/source/components/settings/Settings.js +0 -211
  40. package/dist/source/components/theme/ThemeSwitcher.js +0 -11
  41. package/dist/source/config/themes.config.js +0 -123
  42. package/dist/source/contexts/theme.context.js +0 -29
  43. package/dist/source/hooks/useKeyboard.js +0 -188
  44. package/dist/source/hooks/useKeyboardBlocker.js +0 -45
  45. package/dist/source/hooks/useNavigation.js +0 -5
  46. package/dist/source/hooks/usePlayer.js +0 -43
  47. package/dist/source/hooks/usePlaylist.js +0 -65
  48. package/dist/source/hooks/useSearch.js +0 -76
  49. package/dist/source/hooks/useSleepTimer.js +0 -48
  50. package/dist/source/hooks/useTerminalSize.js +0 -24
  51. package/dist/source/hooks/useTheme.js +0 -5
  52. package/dist/source/hooks/useYouTubeMusic.js +0 -112
  53. package/dist/source/main.js +0 -127
  54. package/dist/source/services/cache/cache.service.js +0 -67
  55. package/dist/source/services/completions/completions.service.js +0 -313
  56. package/dist/source/services/config/config.service.js +0 -191
  57. package/dist/source/services/discord/discord-rpc.service.js +0 -95
  58. package/dist/source/services/download/download.service.js +0 -350
  59. package/dist/source/services/export/export.service.js +0 -131
  60. package/dist/source/services/history/history.service.js +0 -83
  61. package/dist/source/services/import/import.service.js +0 -272
  62. package/dist/source/services/import/spotify.service.js +0 -171
  63. package/dist/source/services/import/track-matcher.service.js +0 -271
  64. package/dist/source/services/import/youtube-import.service.js +0 -84
  65. package/dist/source/services/logger/logger.service.js +0 -52
  66. package/dist/source/services/lyrics/lyrics.service.js +0 -93
  67. package/dist/source/services/mpris/mpris.service.js +0 -78
  68. package/dist/source/services/notification/notification.service.js +0 -57
  69. package/dist/source/services/player/dependency-check.service.js +0 -140
  70. package/dist/source/services/player/player.service.js +0 -478
  71. package/dist/source/services/player-state/player-state.service.js +0 -123
  72. package/dist/source/services/plugin/plugin-audio-api.js +0 -36
  73. package/dist/source/services/plugin/plugin-context.js +0 -256
  74. package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
  75. package/dist/source/services/plugin/plugin-installer.service.js +0 -248
  76. package/dist/source/services/plugin/plugin-loader.service.js +0 -161
  77. package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
  78. package/dist/source/services/plugin/plugin-registry.service.js +0 -215
  79. package/dist/source/services/plugin/plugin-ui-api.js +0 -46
  80. package/dist/source/services/plugin/plugin-updater.service.js +0 -206
  81. package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
  82. package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
  83. package/dist/source/services/version-check/version-check.service.js +0 -121
  84. package/dist/source/services/web/static-file.service.js +0 -185
  85. package/dist/source/services/web/web-server-manager.js +0 -507
  86. package/dist/source/services/web/web-streaming.service.js +0 -292
  87. package/dist/source/services/web/websocket.server.js +0 -267
  88. package/dist/source/services/youtube-music/api.js +0 -649
  89. package/dist/source/services/youtube-music/search.service.js +0 -38
  90. package/dist/source/stores/history.store.js +0 -64
  91. package/dist/source/stores/navigation.store.js +0 -90
  92. package/dist/source/stores/player.store.js +0 -789
  93. package/dist/source/stores/plugins.store.js +0 -177
  94. package/dist/source/types/actions.js +0 -1
  95. package/dist/source/types/cli.types.js +0 -1
  96. package/dist/source/types/config.types.js +0 -1
  97. package/dist/source/types/history.types.js +0 -1
  98. package/dist/source/types/import.types.js +0 -2
  99. package/dist/source/types/keyboard.types.js +0 -1
  100. package/dist/source/types/navigation.types.js +0 -1
  101. package/dist/source/types/player.types.js +0 -1
  102. package/dist/source/types/playlist.types.js +0 -1
  103. package/dist/source/types/plugin.types.js +0 -1
  104. package/dist/source/types/theme.types.js +0 -1
  105. package/dist/source/types/web.types.js +0 -2
  106. package/dist/source/types/youtube-music.types.js +0 -1
  107. package/dist/source/types/youtubei.types.js +0 -3
  108. package/dist/source/utils/constants.js +0 -135
  109. package/dist/source/utils/format.js +0 -24
  110. package/dist/source/utils/icons.js +0 -28
  111. package/dist/source/utils/search-filters.js +0 -100
@@ -1,127 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Main application orchestrator
3
- import { NavigationProvider } from "./stores/navigation.store.js";
4
- import { PluginsProvider } from "./stores/plugins.store.js";
5
- import MainLayout from "./components/layouts/MainLayout.js";
6
- import { ThemeProvider } from "./contexts/theme.context.js";
7
- import { PlayerProvider } from "./stores/player.store.js";
8
- import { HistoryProvider } from "./stores/history.store.js";
9
- import { ErrorBoundary } from "./components/common/ErrorBoundary.js";
10
- import { KeyboardManager } from "./hooks/useKeyboard.js";
11
- import { KeyboardBlockProvider } from "./hooks/useKeyboardBlocker.js";
12
- import { Box, Text } from 'ink';
13
- import { useEffect } from 'react';
14
- import { useNavigation } from "./hooks/useNavigation.js";
15
- import { usePlayer } from "./hooks/usePlayer.js";
16
- import { useYouTubeMusic } from "./hooks/useYouTubeMusic.js";
17
- import { VIEW } from "./utils/constants.js";
18
- import { getConfigService } from "./services/config/config.service.js";
19
- import { getNotificationService } from "./services/notification/notification.service.js";
20
- function Initializer({ flags }) {
21
- const { dispatch } = useNavigation();
22
- const { play } = usePlayer();
23
- const { getTrack, getPlaylist } = useYouTubeMusic();
24
- useEffect(() => {
25
- // Check for background playback state on startup
26
- const config = getConfigService();
27
- const backgroundState = config.getBackgroundPlaybackState();
28
- if (backgroundState.enabled) {
29
- // Show notification about background playback
30
- const notification = getNotificationService();
31
- notification.setEnabled(true);
32
- void notification.notify('Background Playback Active', 'Press Shift+R to resume control');
33
- }
34
- if (flags?.showSuggestions) {
35
- dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
36
- }
37
- else if (flags?.searchQuery) {
38
- dispatch({ category: 'NAVIGATE', view: VIEW.SEARCH });
39
- dispatch({ category: 'SET_SEARCH_QUERY', query: flags.searchQuery });
40
- }
41
- else if (flags?.playTrack) {
42
- void getTrack(flags.playTrack).then(track => {
43
- if (track)
44
- play(track);
45
- });
46
- }
47
- else if (flags?.playPlaylist) {
48
- dispatch({ category: 'NAVIGATE', view: VIEW.PLAYLISTS });
49
- void getPlaylist(flags.playPlaylist).then(playlist => {
50
- // For now just navigate, but we could auto-play
51
- if (playlist) {
52
- dispatch({ category: 'SET_SELECTED_PLAYLIST', index: 0 });
53
- }
54
- });
55
- }
56
- }, [flags, dispatch, play, getTrack, getPlaylist]);
57
- return null;
58
- }
59
- function HeadlessLayout({ flags }) {
60
- const { play, pause, resume, next, previous } = usePlayer();
61
- const { getTrack, getPlaylist, search } = useYouTubeMusic();
62
- useEffect(() => {
63
- void (async () => {
64
- if (flags?.playTrack) {
65
- const track = await getTrack(flags.playTrack);
66
- if (!track) {
67
- console.error(`Track not found: ${flags.playTrack}`);
68
- process.exitCode = 1;
69
- return;
70
- }
71
- play(track);
72
- console.log(`Playing: ${track.title}`);
73
- return;
74
- }
75
- if (flags?.searchQuery) {
76
- const response = await search(flags.searchQuery, {
77
- type: 'songs',
78
- limit: 1,
79
- });
80
- const songResult = response?.results.find(result => result.type === 'song');
81
- if (!songResult) {
82
- console.error(`No playable tracks found for: "${flags.searchQuery}"`);
83
- process.exitCode = 1;
84
- return;
85
- }
86
- const track = songResult.data;
87
- play(track, { clearQueue: true });
88
- console.log(`Playing: ${track.title}`);
89
- return;
90
- }
91
- if (flags?.playPlaylist) {
92
- const playlist = await getPlaylist(flags.playPlaylist);
93
- const firstTrack = playlist?.tracks[0];
94
- if (!firstTrack) {
95
- console.error(`No playable tracks found in playlist: ${flags.playPlaylist}`);
96
- process.exitCode = 1;
97
- return;
98
- }
99
- play(firstTrack, { clearQueue: true });
100
- console.log(`Playing playlist "${playlist.name}": ${firstTrack.title}`);
101
- return;
102
- }
103
- if (flags?.action === 'pause')
104
- pause();
105
- if (flags?.action === 'resume')
106
- resume();
107
- if (flags?.action === 'next')
108
- next();
109
- if (flags?.action === 'previous')
110
- previous();
111
- })();
112
- }, [
113
- flags,
114
- play,
115
- pause,
116
- resume,
117
- next,
118
- previous,
119
- getTrack,
120
- getPlaylist,
121
- search,
122
- ]);
123
- return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "green", children: "Headless mode active." }) }));
124
- }
125
- export default function Main({ flags }) {
126
- return (_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { children: _jsx(PlayerProvider, { children: _jsx(HistoryProvider, { children: _jsx(NavigationProvider, { children: _jsx(PluginsProvider, { children: _jsx(KeyboardBlockProvider, { children: _jsxs(Box, { flexDirection: "column", children: [_jsx(KeyboardManager, {}), flags?.headless ? (_jsx(HeadlessLayout, { flags: flags })) : (_jsxs(_Fragment, { children: [_jsx(Initializer, { flags: flags }), _jsx(MainLayout, {})] }))] }) }) }) }) }) }) }) }));
127
- }
@@ -1,67 +0,0 @@
1
- // In-memory LRU cache with optional TTL for API responses
2
- import { logger } from "../logger/logger.service.js";
3
- export class CacheService {
4
- cache = new Map();
5
- maxSize;
6
- defaultTtlMs;
7
- constructor(maxSize = 100, defaultTtlMs = 5 * 60 * 1000) {
8
- this.maxSize = maxSize;
9
- this.defaultTtlMs = defaultTtlMs;
10
- }
11
- get(key) {
12
- const entry = this.cache.get(key);
13
- if (!entry)
14
- return null;
15
- if (Date.now() > entry.expiresAt) {
16
- this.cache.delete(key);
17
- return null;
18
- }
19
- entry.lastAccessed = Date.now();
20
- return entry.value;
21
- }
22
- set(key, value, ttlMs) {
23
- // Evict LRU entry if at capacity
24
- if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
25
- this.evictLru();
26
- }
27
- this.cache.set(key, {
28
- value,
29
- expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs),
30
- lastAccessed: Date.now(),
31
- });
32
- }
33
- has(key) {
34
- return this.get(key) !== null;
35
- }
36
- delete(key) {
37
- this.cache.delete(key);
38
- }
39
- clear() {
40
- this.cache.clear();
41
- }
42
- get size() {
43
- return this.cache.size;
44
- }
45
- evictLru() {
46
- let lruKey = null;
47
- let lruTime = Infinity;
48
- for (const [key, entry] of this.cache) {
49
- if (entry.lastAccessed < lruTime) {
50
- lruTime = entry.lastAccessed;
51
- lruKey = key;
52
- }
53
- }
54
- if (lruKey) {
55
- logger.debug('CacheService', 'Evicting LRU entry', { key: lruKey });
56
- this.cache.delete(lruKey);
57
- }
58
- }
59
- }
60
- // Shared search result cache (100 entries, 5min TTL)
61
- let searchCacheInstance = null;
62
- export const getSearchCache = () => {
63
- if (!searchCacheInstance) {
64
- searchCacheInstance = new CacheService(100, 5 * 60 * 1000);
65
- }
66
- return searchCacheInstance;
67
- };
@@ -1,313 +0,0 @@
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
- }
@@ -1,191 +0,0 @@
1
- // Configuration management service
2
- import { CONFIG_DIR, CONFIG_FILE } from "../../utils/constants.js";
3
- import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
4
- import { BUILTIN_THEMES, DEFAULT_THEME } from "../../config/themes.config.js";
5
- import path from 'node:path';
6
- class ConfigService {
7
- configPath;
8
- configDir;
9
- config;
10
- constructor() {
11
- this.configDir = CONFIG_DIR;
12
- this.configPath = CONFIG_FILE;
13
- this.config = this.load() || this.getDefaultConfig();
14
- }
15
- getDefaultConfig() {
16
- return {
17
- theme: 'dark',
18
- volume: 70,
19
- keybindings: {},
20
- playlists: [],
21
- history: [],
22
- searchHistory: [],
23
- favorites: [],
24
- repeat: 'off',
25
- shuffle: false,
26
- customTheme: undefined,
27
- streamQuality: 'high',
28
- audioNormalization: false,
29
- notifications: false,
30
- discordRichPresence: false,
31
- downloadsEnabled: false,
32
- downloadDirectory: path.join(CONFIG_DIR, 'downloads'),
33
- downloadFormat: 'mp3',
34
- webServer: {
35
- enabled: false,
36
- host: 'localhost',
37
- port: 8080,
38
- enableCors: true,
39
- allowedOrigins: ['*'],
40
- auth: {
41
- enabled: false,
42
- },
43
- },
44
- gaplessPlayback: true,
45
- crossfadeDuration: 0,
46
- volumeFadeDuration: 0,
47
- equalizerPreset: 'flat',
48
- };
49
- }
50
- load() {
51
- try {
52
- if (!existsSync(this.configPath)) {
53
- return null;
54
- }
55
- const data = readFileSync(this.configPath, 'utf-8');
56
- const config = JSON.parse(data);
57
- // Merge with defaults to handle new fields
58
- return { ...this.getDefaultConfig(), ...config };
59
- }
60
- catch {
61
- return null;
62
- }
63
- }
64
- save() {
65
- try {
66
- // Ensure config directory exists
67
- if (!existsSync(this.configDir)) {
68
- mkdirSync(this.configDir, { recursive: true });
69
- }
70
- writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
71
- }
72
- catch (error) {
73
- console.error('Failed to save config:', error);
74
- }
75
- }
76
- get(key) {
77
- return this.config[key];
78
- }
79
- set(key, value) {
80
- this.config[key] = value;
81
- this.save();
82
- }
83
- updateTheme(themeName) {
84
- this.config.theme = themeName;
85
- this.save();
86
- }
87
- getTheme() {
88
- if (this.config.theme === 'custom' && this.config.customTheme) {
89
- return this.config.customTheme;
90
- }
91
- const builtinTheme = BUILTIN_THEMES[this.config.theme];
92
- if (builtinTheme) {
93
- return builtinTheme;
94
- }
95
- return DEFAULT_THEME;
96
- }
97
- setCustomTheme(theme) {
98
- this.config.customTheme = theme;
99
- this.config.theme = 'custom';
100
- this.save();
101
- }
102
- getKeybinding(action) {
103
- return this.config.keybindings[action]?.keys;
104
- }
105
- setKeybinding(action, keys) {
106
- this.config.keybindings[action] = {
107
- keys,
108
- description: `Custom binding for ${action}`,
109
- };
110
- this.save();
111
- }
112
- addToHistory(trackId) {
113
- // Add to front of history, limit to 1000
114
- this.config.history = [
115
- trackId,
116
- ...this.config.history.filter(id => id !== trackId),
117
- ].slice(0, 1000);
118
- this.save();
119
- }
120
- getHistory() {
121
- return this.config.history;
122
- }
123
- addToSearchHistory(query) {
124
- const trimmed = query.trim();
125
- if (!trimmed)
126
- return;
127
- this.config.searchHistory = [
128
- trimmed,
129
- ...(this.config.searchHistory ?? []).filter(q => q !== trimmed),
130
- ].slice(0, 100);
131
- this.save();
132
- }
133
- getSearchHistory() {
134
- return this.config.searchHistory ?? [];
135
- }
136
- addFavorite(trackId) {
137
- if (!this.config.favorites.includes(trackId)) {
138
- this.config.favorites.push(trackId);
139
- this.save();
140
- }
141
- }
142
- removeFavorite(trackId) {
143
- this.config.favorites = this.config.favorites.filter(id => id !== trackId);
144
- this.save();
145
- }
146
- isFavorite(trackId) {
147
- return this.config.favorites.includes(trackId);
148
- }
149
- getFavorites() {
150
- return this.config.favorites;
151
- }
152
- setBackgroundPlaybackState(state) {
153
- this.config.backgroundPlayback = {
154
- enabled: true,
155
- ipcPath: state.ipcPath,
156
- currentUrl: state.currentUrl,
157
- timestamp: new Date().toISOString(),
158
- };
159
- this.save();
160
- }
161
- getBackgroundPlaybackState() {
162
- if (this.config.backgroundPlayback?.enabled) {
163
- return {
164
- enabled: true,
165
- ipcPath: this.config.backgroundPlayback.ipcPath,
166
- currentUrl: this.config.backgroundPlayback.currentUrl,
167
- timestamp: this.config.backgroundPlayback.timestamp,
168
- };
169
- }
170
- return { enabled: false };
171
- }
172
- clearBackgroundPlaybackState() {
173
- this.config.backgroundPlayback = undefined;
174
- this.save();
175
- }
176
- setLastVersionCheck(timestamp) {
177
- this.config.lastVersionCheck = timestamp;
178
- this.save();
179
- }
180
- getLastVersionCheck() {
181
- return this.config.lastVersionCheck;
182
- }
183
- }
184
- // Singleton instance
185
- let configServiceInstance = null;
186
- export function getConfigService() {
187
- if (!configServiceInstance) {
188
- configServiceInstance = new ConfigService();
189
- }
190
- return configServiceInstance;
191
- }