@involvex/youtube-music-cli 0.0.0
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/README.md +352 -0
- package/dist/eslint.config.d.ts +2 -0
- package/dist/eslint.config.js +55 -0
- package/dist/source/app.d.ts +4 -0
- package/dist/source/app.js +17 -0
- package/dist/source/cli.d.ts +2 -0
- package/dist/source/cli.js +241 -0
- package/dist/source/components/common/ErrorBoundary.d.ts +15 -0
- package/dist/source/components/common/ErrorBoundary.js +22 -0
- package/dist/source/components/common/Help.d.ts +1 -0
- package/dist/source/components/common/Help.js +10 -0
- package/dist/source/components/common/ShortcutsBar.d.ts +1 -0
- package/dist/source/components/common/ShortcutsBar.js +33 -0
- package/dist/source/components/config/ConfigLayout.d.ts +1 -0
- package/dist/source/components/config/ConfigLayout.js +84 -0
- package/dist/source/components/layouts/MainLayout.d.ts +4 -0
- package/dist/source/components/layouts/MainLayout.js +83 -0
- package/dist/source/components/layouts/PlayerLayout.d.ts +1 -0
- package/dist/source/components/layouts/PlayerLayout.js +10 -0
- package/dist/source/components/layouts/PluginsLayout.d.ts +1 -0
- package/dist/source/components/layouts/PluginsLayout.js +77 -0
- package/dist/source/components/layouts/SearchLayout.d.ts +4 -0
- package/dist/source/components/layouts/SearchLayout.js +81 -0
- package/dist/source/components/player/NowPlaying.d.ts +1 -0
- package/dist/source/components/player/NowPlaying.js +21 -0
- package/dist/source/components/player/PlayerControls.d.ts +1 -0
- package/dist/source/components/player/PlayerControls.js +41 -0
- package/dist/source/components/player/ProgressBar.d.ts +1 -0
- package/dist/source/components/player/ProgressBar.js +18 -0
- package/dist/source/components/player/QueueList.d.ts +4 -0
- package/dist/source/components/player/QueueList.js +30 -0
- package/dist/source/components/player/Suggestions.d.ts +1 -0
- package/dist/source/components/player/Suggestions.js +47 -0
- package/dist/source/components/playlist/PlaylistList.d.ts +1 -0
- package/dist/source/components/playlist/PlaylistList.js +11 -0
- package/dist/source/components/plugins/PluginInstallDialog.d.ts +5 -0
- package/dist/source/components/plugins/PluginInstallDialog.js +41 -0
- package/dist/source/components/plugins/PluginsAvailable.d.ts +5 -0
- package/dist/source/components/plugins/PluginsAvailable.js +55 -0
- package/dist/source/components/plugins/PluginsList.d.ts +8 -0
- package/dist/source/components/plugins/PluginsList.js +18 -0
- package/dist/source/components/search/SearchBar.d.ts +8 -0
- package/dist/source/components/search/SearchBar.js +50 -0
- package/dist/source/components/search/SearchResults.d.ts +10 -0
- package/dist/source/components/search/SearchResults.js +111 -0
- package/dist/source/components/settings/Settings.d.ts +1 -0
- package/dist/source/components/settings/Settings.js +42 -0
- package/dist/source/components/theme/ThemeSwitcher.d.ts +1 -0
- package/dist/source/components/theme/ThemeSwitcher.js +11 -0
- package/dist/source/config/themes.config.d.ts +3 -0
- package/dist/source/config/themes.config.js +63 -0
- package/dist/source/contexts/theme.context.d.ts +13 -0
- package/dist/source/contexts/theme.context.js +29 -0
- package/dist/source/hooks/useKeyboard.d.ts +10 -0
- package/dist/source/hooks/useKeyboard.js +104 -0
- package/dist/source/hooks/useNavigation.d.ts +1 -0
- package/dist/source/hooks/useNavigation.js +5 -0
- package/dist/source/hooks/usePlayer.d.ts +23 -0
- package/dist/source/hooks/usePlayer.js +35 -0
- package/dist/source/hooks/usePlaylist.d.ts +8 -0
- package/dist/source/hooks/usePlaylist.js +50 -0
- package/dist/source/hooks/useSearch.d.ts +8 -0
- package/dist/source/hooks/useSearch.js +76 -0
- package/dist/source/hooks/useTerminalSize.d.ts +4 -0
- package/dist/source/hooks/useTerminalSize.js +24 -0
- package/dist/source/hooks/useTheme.d.ts +6 -0
- package/dist/source/hooks/useTheme.js +5 -0
- package/dist/source/hooks/useYouTubeMusic.d.ts +11 -0
- package/dist/source/hooks/useYouTubeMusic.js +112 -0
- package/dist/source/main.d.ts +4 -0
- package/dist/source/main.js +69 -0
- package/dist/source/services/config/config.service.d.ts +26 -0
- package/dist/source/services/config/config.service.js +125 -0
- package/dist/source/services/logger/logger.service.d.ts +10 -0
- package/dist/source/services/logger/logger.service.js +52 -0
- package/dist/source/services/player/player.service.d.ts +58 -0
- package/dist/source/services/player/player.service.js +349 -0
- package/dist/source/services/player-state/player-state.service.d.ts +24 -0
- package/dist/source/services/player-state/player-state.service.js +122 -0
- package/dist/source/services/plugin/plugin-audio-api.d.ts +17 -0
- package/dist/source/services/plugin/plugin-audio-api.js +36 -0
- package/dist/source/services/plugin/plugin-context.d.ts +5 -0
- package/dist/source/services/plugin/plugin-context.js +256 -0
- package/dist/source/services/plugin/plugin-hooks.service.d.ts +62 -0
- package/dist/source/services/plugin/plugin-hooks.service.js +135 -0
- package/dist/source/services/plugin/plugin-installer.service.d.ts +27 -0
- package/dist/source/services/plugin/plugin-installer.service.js +247 -0
- package/dist/source/services/plugin/plugin-loader.service.d.ts +33 -0
- package/dist/source/services/plugin/plugin-loader.service.js +161 -0
- package/dist/source/services/plugin/plugin-permissions.service.d.ts +72 -0
- package/dist/source/services/plugin/plugin-permissions.service.js +194 -0
- package/dist/source/services/plugin/plugin-registry.service.d.ts +76 -0
- package/dist/source/services/plugin/plugin-registry.service.js +215 -0
- package/dist/source/services/plugin/plugin-ui-api.d.ts +25 -0
- package/dist/source/services/plugin/plugin-ui-api.js +46 -0
- package/dist/source/services/plugin/plugin-updater.service.d.ts +23 -0
- package/dist/source/services/plugin/plugin-updater.service.js +206 -0
- package/dist/source/services/youtube-music/api.d.ts +13 -0
- package/dist/source/services/youtube-music/api.js +371 -0
- package/dist/source/services/youtube-music/search.service.d.ts +11 -0
- package/dist/source/services/youtube-music/search.service.js +38 -0
- package/dist/source/stores/navigation.store.d.ts +10 -0
- package/dist/source/stores/navigation.store.js +67 -0
- package/dist/source/stores/player.store.d.ts +28 -0
- package/dist/source/stores/player.store.js +458 -0
- package/dist/source/stores/plugins.store.d.ts +46 -0
- package/dist/source/stores/plugins.store.js +177 -0
- package/dist/source/types/actions.d.ts +119 -0
- package/dist/source/types/actions.js +1 -0
- package/dist/source/types/cli.types.d.ts +14 -0
- package/dist/source/types/cli.types.js +1 -0
- package/dist/source/types/config.types.d.ts +19 -0
- package/dist/source/types/config.types.js +1 -0
- package/dist/source/types/keyboard.types.d.ts +5 -0
- package/dist/source/types/keyboard.types.js +1 -0
- package/dist/source/types/navigation.types.d.ts +14 -0
- package/dist/source/types/navigation.types.js +1 -0
- package/dist/source/types/player.types.d.ts +16 -0
- package/dist/source/types/player.types.js +1 -0
- package/dist/source/types/playlist.types.d.ts +12 -0
- package/dist/source/types/playlist.types.js +1 -0
- package/dist/source/types/plugin.types.d.ts +239 -0
- package/dist/source/types/plugin.types.js +1 -0
- package/dist/source/types/theme.types.d.ts +18 -0
- package/dist/source/types/theme.types.js +1 -0
- package/dist/source/types/youtube-music.types.d.ts +35 -0
- package/dist/source/types/youtube-music.types.js +1 -0
- package/dist/source/types/youtubei.types.d.ts +60 -0
- package/dist/source/types/youtubei.types.js +3 -0
- package/dist/source/utils/constants.d.ts +65 -0
- package/dist/source/utils/constants.js +82 -0
- package/dist/source/utils/format.d.ts +3 -0
- package/dist/source/utils/format.js +24 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +13 -0
- package/package.json +100 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Keyboard input handling hook
|
|
2
|
+
import { useCallback, useEffect } from 'react';
|
|
3
|
+
import { useInput } from 'ink';
|
|
4
|
+
import { logger } from "../services/logger/logger.service.js";
|
|
5
|
+
// Global registry for key handlers
|
|
6
|
+
const registry = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Hook to bind keyboard shortcuts.
|
|
9
|
+
* This uses a centralized manager to avoid multiple useInput calls and memory leaks.
|
|
10
|
+
*/
|
|
11
|
+
export function useKeyBinding(keys, handler) {
|
|
12
|
+
const memoizedHandler = useCallback(handler, [handler]);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const entry = { keys, handler: memoizedHandler };
|
|
15
|
+
registry.add(entry);
|
|
16
|
+
return () => {
|
|
17
|
+
registry.delete(entry);
|
|
18
|
+
};
|
|
19
|
+
}, [keys, memoizedHandler]);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Global Keyboard Manager Component
|
|
23
|
+
* This should be rendered once at the root of the app.
|
|
24
|
+
*/
|
|
25
|
+
export function KeyboardManager() {
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
// Debug logging for key presses (helps diagnose binding issues)
|
|
28
|
+
if (input || key.ctrl || key.meta || key.shift) {
|
|
29
|
+
logger.debug('KeyboardManager', 'Key pressed', {
|
|
30
|
+
input,
|
|
31
|
+
ctrl: key.ctrl,
|
|
32
|
+
meta: key.meta,
|
|
33
|
+
shift: key.shift,
|
|
34
|
+
upArrow: key.upArrow,
|
|
35
|
+
downArrow: key.downArrow,
|
|
36
|
+
leftArrow: key.leftArrow,
|
|
37
|
+
rightArrow: key.rightArrow,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Global quit handling
|
|
41
|
+
if (key.ctrl && input === 'c') {
|
|
42
|
+
// Exit cleanly without clearing screen (let Ink handle cleanup)
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
// Note: Ctrl+L refresh removed to fix scroll-to-top issue
|
|
46
|
+
// Direct ANSI escapes bypass Ink's rendering and cause scrolling problems
|
|
47
|
+
// Dispatch to all registered handlers
|
|
48
|
+
for (const entry of registry) {
|
|
49
|
+
const { keys, handler } = entry;
|
|
50
|
+
for (const binding of keys) {
|
|
51
|
+
const lowerBinding = binding.toLowerCase();
|
|
52
|
+
// Handle special keys
|
|
53
|
+
const isMatch = (lowerBinding === 'escape' && key.escape) ||
|
|
54
|
+
((lowerBinding === 'return' || lowerBinding === 'enter') &&
|
|
55
|
+
key.return) ||
|
|
56
|
+
(lowerBinding === 'backspace' && key.backspace) ||
|
|
57
|
+
(lowerBinding === 'tab' && key.tab) ||
|
|
58
|
+
(lowerBinding === 'up' && key.upArrow) ||
|
|
59
|
+
(lowerBinding === 'down' && key.downArrow) ||
|
|
60
|
+
(lowerBinding === 'left' && key.leftArrow) ||
|
|
61
|
+
(lowerBinding === 'right' && key.rightArrow) ||
|
|
62
|
+
(lowerBinding === 'pageup' && key.pageUp) ||
|
|
63
|
+
(lowerBinding === 'pagedown' && key.pageDown) ||
|
|
64
|
+
// Handle combinations
|
|
65
|
+
(() => {
|
|
66
|
+
const parts = lowerBinding.split('+');
|
|
67
|
+
const hasCtrl = parts.includes('ctrl');
|
|
68
|
+
const hasMeta = parts.includes('meta') || parts.includes('alt');
|
|
69
|
+
const hasShift = parts.includes('shift');
|
|
70
|
+
const mainKey = parts[parts.length - 1];
|
|
71
|
+
if (hasCtrl && !key.ctrl)
|
|
72
|
+
return false;
|
|
73
|
+
if (hasMeta && !key.meta)
|
|
74
|
+
return false;
|
|
75
|
+
if (hasShift && !key.shift)
|
|
76
|
+
return false;
|
|
77
|
+
// Check the actual key
|
|
78
|
+
if (mainKey === 'up' && key.upArrow)
|
|
79
|
+
return true;
|
|
80
|
+
if (mainKey === 'down' && key.downArrow)
|
|
81
|
+
return true;
|
|
82
|
+
if (mainKey === 'left' && key.leftArrow)
|
|
83
|
+
return true;
|
|
84
|
+
if (mainKey === 'right' && key.rightArrow)
|
|
85
|
+
return true;
|
|
86
|
+
// Handle '=' and '+' specially (+ is shift+=)
|
|
87
|
+
if (mainKey === '=' && input === '=')
|
|
88
|
+
return true;
|
|
89
|
+
if (mainKey === '+' && input === '+')
|
|
90
|
+
return true;
|
|
91
|
+
if (mainKey === '+' && key.shift && input === '=')
|
|
92
|
+
return true; // shift+= produces '+'
|
|
93
|
+
return input.toLowerCase() === mainKey && !key.ctrl && !key.meta;
|
|
94
|
+
})();
|
|
95
|
+
if (isMatch) {
|
|
96
|
+
handler();
|
|
97
|
+
// We don't break here because multiple handlers might want to react
|
|
98
|
+
// but usually only one does.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useNavigation(): import("../stores/navigation.store.tsx").NavigationContextValue;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Track } from '../types/youtube-music.types.ts';
|
|
2
|
+
export declare function usePlayer(): {
|
|
3
|
+
state: import("../types/player.types.ts").PlayerState;
|
|
4
|
+
dispatch: (action: import("../types/player.types.ts").PlayerAction) => void;
|
|
5
|
+
play: (track: Track, options?: {
|
|
6
|
+
clearQueue?: boolean;
|
|
7
|
+
}) => void;
|
|
8
|
+
pause: () => void;
|
|
9
|
+
resume: () => void;
|
|
10
|
+
next: () => void;
|
|
11
|
+
previous: () => void;
|
|
12
|
+
seek: (position: number) => void;
|
|
13
|
+
setVolume: (volume: number) => void;
|
|
14
|
+
volumeUp: () => void;
|
|
15
|
+
volumeDown: () => void;
|
|
16
|
+
toggleShuffle: () => void;
|
|
17
|
+
toggleRepeat: () => void;
|
|
18
|
+
setQueue: (queue: Track[]) => void;
|
|
19
|
+
addToQueue: (track: Track) => void;
|
|
20
|
+
removeFromQueue: (index: number) => void;
|
|
21
|
+
clearQueue: () => void;
|
|
22
|
+
setQueuePosition: (position: number) => void;
|
|
23
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Player hook - audio playback orchestration
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { usePlayer as usePlayerStore } from "../stores/player.store.js";
|
|
4
|
+
import { getConfigService } from "../services/config/config.service.js";
|
|
5
|
+
export function usePlayer() {
|
|
6
|
+
const { state, dispatch, ...playerStore } = usePlayerStore();
|
|
7
|
+
const play = useCallback((track, options) => {
|
|
8
|
+
// Clear queue if requested (e.g., playing from search results)
|
|
9
|
+
if (options?.clearQueue) {
|
|
10
|
+
dispatch({ category: 'CLEAR_QUEUE' });
|
|
11
|
+
}
|
|
12
|
+
// Add to queue if not already there
|
|
13
|
+
const isInQueue = state.queue.some(t => t.videoId === track.videoId);
|
|
14
|
+
if (!isInQueue) {
|
|
15
|
+
dispatch({ category: 'ADD_TO_QUEUE', track });
|
|
16
|
+
}
|
|
17
|
+
// Find position and play
|
|
18
|
+
const position = state.queue.findIndex(t => t.videoId === track.videoId);
|
|
19
|
+
if (position >= 0) {
|
|
20
|
+
dispatch({ category: 'SET_QUEUE_POSITION', position });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
dispatch({ category: 'PLAY', track });
|
|
24
|
+
}
|
|
25
|
+
// Add to history
|
|
26
|
+
const config = getConfigService();
|
|
27
|
+
config.addToHistory(track.videoId);
|
|
28
|
+
}, [state.queue, dispatch]);
|
|
29
|
+
return {
|
|
30
|
+
...playerStore,
|
|
31
|
+
state,
|
|
32
|
+
dispatch,
|
|
33
|
+
play,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Playlist, Track } from '../types/youtube-music.types.ts';
|
|
2
|
+
export declare function usePlaylist(): {
|
|
3
|
+
playlists: Playlist[];
|
|
4
|
+
createPlaylist: (name: string) => void;
|
|
5
|
+
deletePlaylist: (playlistId: string) => void;
|
|
6
|
+
addTrackToPlaylist: (playlistId: string, track: Track) => void;
|
|
7
|
+
removeTrackFromPlaylist: (playlistId: string, trackIndex: number) => void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Playlist management hook
|
|
2
|
+
import { getConfigService } from "../services/config/config.service.js";
|
|
3
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
export function usePlaylist() {
|
|
5
|
+
const [playlists, setPlaylists] = useState([]);
|
|
6
|
+
const configService = getConfigService();
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setPlaylists(configService.get('playlists'));
|
|
9
|
+
}, []);
|
|
10
|
+
const createPlaylist = useCallback((name) => {
|
|
11
|
+
const newPlaylist = {
|
|
12
|
+
playlistId: Date.now().toString(),
|
|
13
|
+
name,
|
|
14
|
+
tracks: [],
|
|
15
|
+
};
|
|
16
|
+
const updatedPlaylists = [...playlists, newPlaylist];
|
|
17
|
+
setPlaylists(updatedPlaylists);
|
|
18
|
+
configService.set('playlists', updatedPlaylists);
|
|
19
|
+
}, [playlists, configService]);
|
|
20
|
+
const deletePlaylist = useCallback((playlistId) => {
|
|
21
|
+
const updatedPlaylists = playlists.filter(p => p.playlistId !== playlistId);
|
|
22
|
+
setPlaylists(updatedPlaylists);
|
|
23
|
+
configService.set('playlists', updatedPlaylists);
|
|
24
|
+
}, [playlists, configService]);
|
|
25
|
+
const addTrackToPlaylist = useCallback((playlistId, track) => {
|
|
26
|
+
const playlistIndex = playlists.findIndex(p => p.playlistId === playlistId);
|
|
27
|
+
if (playlistIndex === -1)
|
|
28
|
+
return;
|
|
29
|
+
const updatedPlaylists = [...playlists];
|
|
30
|
+
updatedPlaylists[playlistIndex].tracks.push(track);
|
|
31
|
+
setPlaylists(updatedPlaylists);
|
|
32
|
+
configService.set('playlists', updatedPlaylists);
|
|
33
|
+
}, [playlists, configService]);
|
|
34
|
+
const removeTrackFromPlaylist = useCallback((playlistId, trackIndex) => {
|
|
35
|
+
const playlistIndex = playlists.findIndex(p => p.playlistId === playlistId);
|
|
36
|
+
if (playlistIndex === -1)
|
|
37
|
+
return;
|
|
38
|
+
const updatedPlaylists = [...playlists];
|
|
39
|
+
updatedPlaylists[playlistIndex].tracks.splice(trackIndex, 1);
|
|
40
|
+
setPlaylists(updatedPlaylists);
|
|
41
|
+
configService.set('playlists', updatedPlaylists);
|
|
42
|
+
}, [playlists, configService]);
|
|
43
|
+
return {
|
|
44
|
+
playlists,
|
|
45
|
+
createPlaylist,
|
|
46
|
+
deletePlaylist,
|
|
47
|
+
addTrackToPlaylist,
|
|
48
|
+
removeTrackFromPlaylist,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function useSearch(): {
|
|
2
|
+
isLoading: boolean;
|
|
3
|
+
error: string | null;
|
|
4
|
+
searchSongs: (query: string) => Promise<import("../types/youtube-music.types.ts").Track[]>;
|
|
5
|
+
searchAlbums: (query: string) => Promise<import("../types/youtube-music.types.ts").Album[]>;
|
|
6
|
+
searchArtists: (query: string) => Promise<import("../types/youtube-music.types.ts").Artist[]>;
|
|
7
|
+
searchPlaylists: (query: string) => Promise<import("../types/youtube-music.types.ts").Playlist[]>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Search hook
|
|
2
|
+
import { getSearchService } from "../services/youtube-music/search.service.js";
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
export function useSearch() {
|
|
5
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const searchService = getSearchService();
|
|
8
|
+
const searchSongs = useCallback(async (query) => {
|
|
9
|
+
setIsLoading(true);
|
|
10
|
+
setError(null);
|
|
11
|
+
try {
|
|
12
|
+
const results = await searchService.searchSongs(query);
|
|
13
|
+
return results;
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
setIsLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, [searchService]);
|
|
23
|
+
const searchAlbums = useCallback(async (query) => {
|
|
24
|
+
setIsLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
try {
|
|
27
|
+
const results = await searchService.searchAlbums(query);
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
setIsLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}, [searchService]);
|
|
38
|
+
const searchArtists = useCallback(async (query) => {
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
try {
|
|
42
|
+
const results = await searchService.searchArtists(query);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setIsLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}, [searchService]);
|
|
53
|
+
const searchPlaylists = useCallback(async (query) => {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
try {
|
|
57
|
+
const results = await searchService.searchPlaylists(query);
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setIsLoading(false);
|
|
66
|
+
}
|
|
67
|
+
}, [searchService]);
|
|
68
|
+
return {
|
|
69
|
+
isLoading,
|
|
70
|
+
error,
|
|
71
|
+
searchSongs,
|
|
72
|
+
searchAlbums,
|
|
73
|
+
searchArtists,
|
|
74
|
+
searchPlaylists,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useStdout } from 'ink';
|
|
3
|
+
export function useTerminalSize() {
|
|
4
|
+
const { stdout } = useStdout();
|
|
5
|
+
const [size, setSize] = useState({
|
|
6
|
+
columns: stdout?.columns || 80,
|
|
7
|
+
rows: stdout?.rows || 24,
|
|
8
|
+
});
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!stdout)
|
|
11
|
+
return;
|
|
12
|
+
const onResize = () => {
|
|
13
|
+
setSize({
|
|
14
|
+
columns: stdout.columns,
|
|
15
|
+
rows: stdout.rows,
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
stdout.on('resize', onResize);
|
|
19
|
+
return () => {
|
|
20
|
+
stdout.off('resize', onResize);
|
|
21
|
+
};
|
|
22
|
+
}, [stdout]);
|
|
23
|
+
return size;
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SearchOptions, SearchResponse, Track, Album, Artist, Playlist } from '../types/youtube-music.types.ts';
|
|
2
|
+
export declare function useYouTubeMusic(): {
|
|
3
|
+
isLoading: boolean;
|
|
4
|
+
error: string | null;
|
|
5
|
+
search: (query: string, options?: SearchOptions) => Promise<SearchResponse | null>;
|
|
6
|
+
getTrack: (videoId: string) => Promise<Track | null>;
|
|
7
|
+
getAlbum: (albumId: string) => Promise<Album | null>;
|
|
8
|
+
getArtist: (artistId: string) => Promise<Artist | null>;
|
|
9
|
+
getPlaylist: (playlistId: string) => Promise<Playlist | null>;
|
|
10
|
+
getSuggestions: (trackId: string) => Promise<Track[]>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getMusicService } from "../services/youtube-music/api.js";
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
export function useYouTubeMusic() {
|
|
4
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
const musicService = getMusicService();
|
|
7
|
+
const search = useCallback(async (query, options = {}) => {
|
|
8
|
+
setIsLoading(true);
|
|
9
|
+
setError(null);
|
|
10
|
+
try {
|
|
11
|
+
const response = await musicService.search(query, options);
|
|
12
|
+
return response;
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
setIsLoading(false);
|
|
20
|
+
}
|
|
21
|
+
}, [musicService]);
|
|
22
|
+
const getTrack = useCallback(async (videoId) => {
|
|
23
|
+
setIsLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
const track = await musicService.getTrack(videoId);
|
|
27
|
+
return track;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
setError(err instanceof Error ? err.message : 'Failed to get track');
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}, [musicService]);
|
|
37
|
+
const getAlbum = useCallback(async (albumId) => {
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
try {
|
|
41
|
+
const album = await musicService.getAlbum(albumId);
|
|
42
|
+
return album;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
setError(err instanceof Error ? err.message : 'Failed to get album');
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, [musicService]);
|
|
52
|
+
const getArtist = useCallback(async (artistId) => {
|
|
53
|
+
setIsLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
try {
|
|
56
|
+
const artist = await musicService.getArtist(artistId);
|
|
57
|
+
return artist;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
setError(err instanceof Error ? err.message : 'Failed to get artist');
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
setIsLoading(false);
|
|
65
|
+
}
|
|
66
|
+
}, [musicService]);
|
|
67
|
+
const getPlaylist = useCallback(async (playlistId) => {
|
|
68
|
+
setIsLoading(true);
|
|
69
|
+
setError(null);
|
|
70
|
+
try {
|
|
71
|
+
const playlist = await musicService.getPlaylist(playlistId);
|
|
72
|
+
return playlist;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
setError(err instanceof Error ? err.message : 'Failed to get playlist');
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
setIsLoading(false);
|
|
80
|
+
}
|
|
81
|
+
}, [musicService]);
|
|
82
|
+
const getSuggestions = useCallback(async (trackId) => {
|
|
83
|
+
setIsLoading(true);
|
|
84
|
+
setError(null);
|
|
85
|
+
try {
|
|
86
|
+
const suggestions = await musicService.getSuggestions(trackId);
|
|
87
|
+
return suggestions;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Suppress YouTubeJS parsing errors (library limitation with YouTube's changing API)
|
|
91
|
+
// These are not user-actionable and create noise in the UI
|
|
92
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to get suggestions';
|
|
93
|
+
if (!errorMessage.includes('ParsingError')) {
|
|
94
|
+
setError(errorMessage);
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
setIsLoading(false);
|
|
100
|
+
}
|
|
101
|
+
}, [musicService]);
|
|
102
|
+
return {
|
|
103
|
+
isLoading,
|
|
104
|
+
error,
|
|
105
|
+
search,
|
|
106
|
+
getTrack,
|
|
107
|
+
getAlbum,
|
|
108
|
+
getArtist,
|
|
109
|
+
getPlaylist,
|
|
110
|
+
getSuggestions,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { ErrorBoundary } from "./components/common/ErrorBoundary.js";
|
|
9
|
+
import { KeyboardManager } from "./hooks/useKeyboard.js";
|
|
10
|
+
import { Box, Text } from 'ink';
|
|
11
|
+
import { useEffect } from 'react';
|
|
12
|
+
import { useNavigation } from "./hooks/useNavigation.js";
|
|
13
|
+
import { usePlayer } from "./hooks/usePlayer.js";
|
|
14
|
+
import { useYouTubeMusic } from "./hooks/useYouTubeMusic.js";
|
|
15
|
+
import { VIEW } from "./utils/constants.js";
|
|
16
|
+
function Initializer({ flags }) {
|
|
17
|
+
const { dispatch } = useNavigation();
|
|
18
|
+
const { play } = usePlayer();
|
|
19
|
+
const { getTrack, getPlaylist } = useYouTubeMusic();
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (flags?.showSuggestions) {
|
|
22
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
|
|
23
|
+
}
|
|
24
|
+
else if (flags?.searchQuery) {
|
|
25
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.SEARCH });
|
|
26
|
+
dispatch({ category: 'SET_SEARCH_QUERY', query: flags.searchQuery });
|
|
27
|
+
}
|
|
28
|
+
else if (flags?.playTrack) {
|
|
29
|
+
void getTrack(flags.playTrack).then(track => {
|
|
30
|
+
if (track)
|
|
31
|
+
play(track);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else if (flags?.playPlaylist) {
|
|
35
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.PLAYLISTS });
|
|
36
|
+
void getPlaylist(flags.playPlaylist).then(playlist => {
|
|
37
|
+
// For now just navigate, but we could auto-play
|
|
38
|
+
if (playlist) {
|
|
39
|
+
dispatch({ category: 'SET_SELECTED_PLAYLIST', index: 0 });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}, [flags, dispatch, play, getTrack, getPlaylist]);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function HeadlessLayout({ flags }) {
|
|
47
|
+
const { play, pause, resume, next, previous } = usePlayer();
|
|
48
|
+
const { getTrack } = useYouTubeMusic();
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (flags?.playTrack) {
|
|
51
|
+
void getTrack(flags.playTrack).then(track => {
|
|
52
|
+
if (track)
|
|
53
|
+
play(track);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (flags?.action === 'pause')
|
|
57
|
+
pause();
|
|
58
|
+
if (flags?.action === 'resume')
|
|
59
|
+
resume();
|
|
60
|
+
if (flags?.action === 'next')
|
|
61
|
+
next();
|
|
62
|
+
if (flags?.action === 'previous')
|
|
63
|
+
previous();
|
|
64
|
+
}, [flags, play, pause, resume, next, previous, getTrack]);
|
|
65
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "green", children: "Headless mode active." }) }));
|
|
66
|
+
}
|
|
67
|
+
export default function Main({ flags }) {
|
|
68
|
+
return (_jsx(ErrorBoundary, { children: _jsx(ThemeProvider, { children: _jsx(PlayerProvider, { children: _jsx(NavigationProvider, { children: _jsx(PluginsProvider, { children: _jsxs(Box, { flexDirection: "column", children: [_jsx(KeyboardManager, {}), flags?.headless ? (_jsx(HeadlessLayout, { flags: flags })) : (_jsxs(_Fragment, { children: [_jsx(Initializer, { flags: flags }), _jsx(MainLayout, {})] }))] }) }) }) }) }) }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Config } from '../../types/config.types.ts';
|
|
2
|
+
import type { Theme } from '../../types/theme.types.ts';
|
|
3
|
+
declare class ConfigService {
|
|
4
|
+
private configPath;
|
|
5
|
+
private configDir;
|
|
6
|
+
private config;
|
|
7
|
+
constructor();
|
|
8
|
+
getDefaultConfig(): Config;
|
|
9
|
+
load(): Config | null;
|
|
10
|
+
save(): void;
|
|
11
|
+
get<K extends keyof Config>(key: K): Config[K];
|
|
12
|
+
set<K extends keyof Config>(key: K, value: Config[K]): void;
|
|
13
|
+
updateTheme(themeName: string): void;
|
|
14
|
+
getTheme(): Theme;
|
|
15
|
+
setCustomTheme(theme: Theme): void;
|
|
16
|
+
getKeybinding(action: string): string[] | undefined;
|
|
17
|
+
setKeybinding(action: string, keys: string[]): void;
|
|
18
|
+
addToHistory(trackId: string): void;
|
|
19
|
+
getHistory(): string[];
|
|
20
|
+
addFavorite(trackId: string): void;
|
|
21
|
+
removeFavorite(trackId: string): void;
|
|
22
|
+
isFavorite(trackId: string): boolean;
|
|
23
|
+
getFavorites(): string[];
|
|
24
|
+
}
|
|
25
|
+
export declare function getConfigService(): ConfigService;
|
|
26
|
+
export {};
|