@involvex/youtube-music-cli 0.0.46 → 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.
- package/CHANGELOG.md +4 -0
- package/dist/cli.js.map +1004 -0
- package/dist/source/hooks/usePlayer.d.ts +1 -0
- package/dist/source/services/player-state/player-state.service.d.ts +1 -0
- package/dist/source/stores/player.store.d.ts +1 -0
- package/dist/source/types/actions.d.ts +4 -0
- package/dist/source/types/player.types.d.ts +3 -2
- package/dist/source/utils/constants.d.ts +1 -0
- package/dist/source/utils/icons.d.ts +1 -0
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
- package/dist/eslint.config.js +0 -55
- package/dist/package.json +0 -120
- package/dist/scripts/build-cli.js +0 -46
- package/dist/source/app.js +0 -17
- package/dist/source/cli.js +0 -504
- package/dist/source/components/common/ErrorBoundary.js +0 -22
- package/dist/source/components/common/Help.js +0 -18
- package/dist/source/components/common/ShortcutsBar.js +0 -80
- package/dist/source/components/config/ConfigLayout.js +0 -84
- package/dist/source/components/config/KeybindingsLayout.js +0 -107
- package/dist/source/components/export/ExportLayout.js +0 -111
- package/dist/source/components/import/ImportLayout.js +0 -119
- package/dist/source/components/import/ImportProgress.js +0 -73
- package/dist/source/components/layouts/ExploreLayout.js +0 -72
- package/dist/source/components/layouts/HistoryLayout.js +0 -37
- package/dist/source/components/layouts/LyricsLayout.js +0 -89
- package/dist/source/components/layouts/MainLayout.js +0 -190
- package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
- package/dist/source/components/layouts/PlayerLayout.js +0 -9
- package/dist/source/components/layouts/PluginsLayout.js +0 -77
- package/dist/source/components/layouts/SearchLayout.js +0 -193
- package/dist/source/components/layouts/TrendingLayout.js +0 -59
- package/dist/source/components/player/NowPlaying.js +0 -45
- package/dist/source/components/player/PlayerControls.js +0 -83
- package/dist/source/components/player/ProgressBar.js +0 -19
- package/dist/source/components/player/QueueList.js +0 -36
- package/dist/source/components/player/Suggestions.js +0 -50
- package/dist/source/components/playlist/PlaylistList.js +0 -138
- package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
- package/dist/source/components/plugins/PluginsAvailable.js +0 -55
- package/dist/source/components/plugins/PluginsList.js +0 -18
- package/dist/source/components/search/SearchBar.js +0 -55
- package/dist/source/components/search/SearchHistory.js +0 -35
- package/dist/source/components/search/SearchResults.js +0 -280
- package/dist/source/components/settings/Settings.js +0 -211
- package/dist/source/components/theme/ThemeSwitcher.js +0 -11
- package/dist/source/config/themes.config.js +0 -123
- package/dist/source/contexts/theme.context.js +0 -29
- package/dist/source/hooks/useKeyboard.js +0 -188
- package/dist/source/hooks/useKeyboardBlocker.js +0 -45
- package/dist/source/hooks/useNavigation.js +0 -5
- package/dist/source/hooks/usePlayer.js +0 -43
- package/dist/source/hooks/usePlaylist.js +0 -65
- package/dist/source/hooks/useSearch.js +0 -76
- package/dist/source/hooks/useSleepTimer.js +0 -48
- package/dist/source/hooks/useTerminalSize.js +0 -24
- package/dist/source/hooks/useTheme.js +0 -5
- package/dist/source/hooks/useYouTubeMusic.js +0 -112
- package/dist/source/main.js +0 -127
- package/dist/source/services/cache/cache.service.js +0 -67
- package/dist/source/services/completions/completions.service.js +0 -313
- package/dist/source/services/config/config.service.js +0 -191
- package/dist/source/services/discord/discord-rpc.service.js +0 -95
- package/dist/source/services/download/download.service.js +0 -350
- package/dist/source/services/export/export.service.js +0 -131
- package/dist/source/services/history/history.service.js +0 -83
- package/dist/source/services/import/import.service.js +0 -272
- package/dist/source/services/import/spotify.service.js +0 -171
- package/dist/source/services/import/track-matcher.service.js +0 -271
- package/dist/source/services/import/youtube-import.service.js +0 -84
- package/dist/source/services/logger/logger.service.js +0 -52
- package/dist/source/services/lyrics/lyrics.service.js +0 -93
- package/dist/source/services/mpris/mpris.service.js +0 -78
- package/dist/source/services/notification/notification.service.js +0 -57
- package/dist/source/services/player/dependency-check.service.js +0 -140
- package/dist/source/services/player/player.service.js +0 -478
- package/dist/source/services/player-state/player-state.service.js +0 -122
- package/dist/source/services/plugin/plugin-audio-api.js +0 -36
- package/dist/source/services/plugin/plugin-context.js +0 -256
- package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
- package/dist/source/services/plugin/plugin-installer.service.js +0 -248
- package/dist/source/services/plugin/plugin-loader.service.js +0 -161
- package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
- package/dist/source/services/plugin/plugin-registry.service.js +0 -215
- package/dist/source/services/plugin/plugin-ui-api.js +0 -46
- package/dist/source/services/plugin/plugin-updater.service.js +0 -206
- package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
- package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
- package/dist/source/services/version-check/version-check.service.js +0 -121
- package/dist/source/services/web/static-file.service.js +0 -185
- package/dist/source/services/web/web-server-manager.js +0 -506
- package/dist/source/services/web/web-streaming.service.js +0 -290
- package/dist/source/services/web/websocket.server.js +0 -267
- package/dist/source/services/youtube-music/api.js +0 -649
- package/dist/source/services/youtube-music/search.service.js +0 -38
- package/dist/source/stores/history.store.js +0 -64
- package/dist/source/stores/navigation.store.js +0 -90
- package/dist/source/stores/player.store.js +0 -724
- package/dist/source/stores/plugins.store.js +0 -177
- package/dist/source/types/actions.js +0 -1
- package/dist/source/types/cli.types.js +0 -1
- package/dist/source/types/config.types.js +0 -1
- package/dist/source/types/history.types.js +0 -1
- package/dist/source/types/import.types.js +0 -2
- package/dist/source/types/keyboard.types.js +0 -1
- package/dist/source/types/navigation.types.js +0 -1
- package/dist/source/types/player.types.js +0 -1
- package/dist/source/types/playlist.types.js +0 -1
- package/dist/source/types/plugin.types.js +0 -1
- package/dist/source/types/theme.types.js +0 -1
- package/dist/source/types/web.types.js +0 -2
- package/dist/source/types/youtube-music.types.js +0 -1
- package/dist/source/types/youtubei.types.js +0 -3
- package/dist/source/utils/constants.js +0 -134
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -26
- package/dist/source/utils/search-filters.js +0 -100
package/dist/source/main.js
DELETED
|
@@ -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
|
-
}
|