@involvex/youtube-music-cli 0.0.47 → 0.0.49
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 +8 -0
- package/dist/cli.js.map +6 -6
- 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 -89
- 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 -123
- 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 -507
- package/dist/source/services/web/web-streaming.service.js +0 -292
- 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 -789
- 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 -135
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -28
- package/dist/source/utils/search-filters.js +0 -100
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
// Keyboard input handling hook
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { useInput } from 'ink';
|
|
4
|
-
import { logger } from "../services/logger/logger.service.js";
|
|
5
|
-
import { useKeyboardBlockContext } from "./useKeyboardBlocker.js";
|
|
6
|
-
// Global registry for key handlers
|
|
7
|
-
const registry = new Set();
|
|
8
|
-
/**
|
|
9
|
-
* Hook to bind keyboard shortcuts.
|
|
10
|
-
* This uses a centralized manager to avoid multiple useInput calls and memory leaks.
|
|
11
|
-
* Uses a ref-based approach to always call the latest handler without stale closures.
|
|
12
|
-
*/
|
|
13
|
-
export function useKeyBinding(keys, handler, options) {
|
|
14
|
-
const handlerRef = useRef(handler);
|
|
15
|
-
handlerRef.current = handler;
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const entry = {
|
|
18
|
-
keys,
|
|
19
|
-
handler: () => handlerRef.current(),
|
|
20
|
-
bypassBlock: options?.bypassBlock,
|
|
21
|
-
};
|
|
22
|
-
registry.add(entry);
|
|
23
|
-
return () => {
|
|
24
|
-
registry.delete(entry);
|
|
25
|
-
};
|
|
26
|
-
}, [keys, options?.bypassBlock]); // keys and bypassBlock are deps; handlerRef is a stable ref
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Global Keyboard Manager Component
|
|
30
|
-
* This should be rendered once at the root of the app.
|
|
31
|
-
*/
|
|
32
|
-
export function KeyboardManager() {
|
|
33
|
-
const { blockCount } = useKeyboardBlockContext();
|
|
34
|
-
useInput((input, key) => {
|
|
35
|
-
if (blockCount > 0) {
|
|
36
|
-
// When keyboard input is blocked (e.g., within a focused text input),
|
|
37
|
-
// check if any entry has bypassBlock flag and matches this key.
|
|
38
|
-
for (const entry of registry) {
|
|
39
|
-
if (entry.bypassBlock) {
|
|
40
|
-
for (const binding of entry.keys) {
|
|
41
|
-
const lowerBinding = binding.toLowerCase();
|
|
42
|
-
// Check for ESC key (most common bypass case)
|
|
43
|
-
if (lowerBinding === 'escape' && key.escape) {
|
|
44
|
-
entry.handler();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
// Handle other bypass keys
|
|
48
|
-
const isMatch = ((lowerBinding === 'return' || lowerBinding === 'enter') &&
|
|
49
|
-
key.return) ||
|
|
50
|
-
(lowerBinding === 'backspace' && key.backspace) ||
|
|
51
|
-
(lowerBinding === 'tab' && key.tab) ||
|
|
52
|
-
(lowerBinding === 'up' && key.upArrow) ||
|
|
53
|
-
(lowerBinding === 'down' && key.downArrow) ||
|
|
54
|
-
(lowerBinding === 'left' && key.leftArrow) ||
|
|
55
|
-
(lowerBinding === 'right' && key.rightArrow) ||
|
|
56
|
-
(lowerBinding === 'pageup' && key.pageUp) ||
|
|
57
|
-
(lowerBinding === 'pagedown' && key.pageDown) ||
|
|
58
|
-
(() => {
|
|
59
|
-
const parts = lowerBinding.split('+');
|
|
60
|
-
const hasCtrl = parts.includes('ctrl');
|
|
61
|
-
const hasMeta = parts.includes('meta') || parts.includes('alt');
|
|
62
|
-
const hasShift = parts.includes('shift');
|
|
63
|
-
const mainKey = parts[parts.length - 1];
|
|
64
|
-
if (hasCtrl && !key.ctrl)
|
|
65
|
-
return false;
|
|
66
|
-
if (hasMeta && !key.meta)
|
|
67
|
-
return false;
|
|
68
|
-
if (hasShift && !key.shift)
|
|
69
|
-
return false;
|
|
70
|
-
// Check arrow keys
|
|
71
|
-
if (mainKey === 'up' && key.upArrow)
|
|
72
|
-
return true;
|
|
73
|
-
if (mainKey === 'down' && key.downArrow)
|
|
74
|
-
return true;
|
|
75
|
-
if (mainKey === 'left' && key.leftArrow)
|
|
76
|
-
return true;
|
|
77
|
-
if (mainKey === 'right' && key.rightArrow)
|
|
78
|
-
return true;
|
|
79
|
-
// Handle '=' and '+'
|
|
80
|
-
if (mainKey === '=' && input === '=')
|
|
81
|
-
return true;
|
|
82
|
-
if (mainKey === '+' && input === '+')
|
|
83
|
-
return true;
|
|
84
|
-
if (mainKey === '+' && key.shift && input === '=')
|
|
85
|
-
return true;
|
|
86
|
-
return (input.toLowerCase() === mainKey && !key.ctrl && !key.meta);
|
|
87
|
-
})();
|
|
88
|
-
if (isMatch) {
|
|
89
|
-
entry.handler();
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// If no bypass handler matched, skip all global shortcuts
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
// Debug logging for key presses (helps diagnose binding issues)
|
|
99
|
-
if (input || key.ctrl || key.meta || key.shift) {
|
|
100
|
-
logger.debug('KeyboardManager', 'Key pressed', {
|
|
101
|
-
input,
|
|
102
|
-
ctrl: key.ctrl,
|
|
103
|
-
meta: key.meta,
|
|
104
|
-
shift: key.shift,
|
|
105
|
-
upArrow: key.upArrow,
|
|
106
|
-
downArrow: key.downArrow,
|
|
107
|
-
leftArrow: key.leftArrow,
|
|
108
|
-
rightArrow: key.rightArrow,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
// Global quit handling
|
|
112
|
-
if (key.ctrl && input === 'c') {
|
|
113
|
-
// Exit cleanly without clearing screen (let Ink handle cleanup)
|
|
114
|
-
process.exit(0);
|
|
115
|
-
}
|
|
116
|
-
// Note: Ctrl+L refresh removed to fix scroll-to-top issue
|
|
117
|
-
// Direct ANSI escapes bypass Ink's rendering and cause scrolling problems
|
|
118
|
-
// Dispatch to all registered handlers
|
|
119
|
-
for (const entry of registry) {
|
|
120
|
-
const { keys, handler } = entry;
|
|
121
|
-
for (const binding of keys) {
|
|
122
|
-
const lowerBinding = binding.toLowerCase();
|
|
123
|
-
// Handle special keys
|
|
124
|
-
const isMatch = (lowerBinding === 'escape' && key.escape) ||
|
|
125
|
-
((lowerBinding === 'return' || lowerBinding === 'enter') &&
|
|
126
|
-
key.return) ||
|
|
127
|
-
(lowerBinding === 'backspace' && key.backspace) ||
|
|
128
|
-
(lowerBinding === 'tab' && key.tab) ||
|
|
129
|
-
(lowerBinding === 'up' && key.upArrow) ||
|
|
130
|
-
(lowerBinding === 'down' && key.downArrow) ||
|
|
131
|
-
(lowerBinding === 'left' && key.leftArrow) ||
|
|
132
|
-
(lowerBinding === 'right' && key.rightArrow) ||
|
|
133
|
-
(lowerBinding === 'pageup' && key.pageUp) ||
|
|
134
|
-
(lowerBinding === 'pagedown' && key.pageDown) ||
|
|
135
|
-
// Handle combinations
|
|
136
|
-
(() => {
|
|
137
|
-
const parts = lowerBinding.split('+');
|
|
138
|
-
const hasCtrl = parts.includes('ctrl');
|
|
139
|
-
const hasMeta = parts.includes('meta') || parts.includes('alt');
|
|
140
|
-
const hasShift = parts.includes('shift');
|
|
141
|
-
const mainKey = parts[parts.length - 1];
|
|
142
|
-
const uppercaseShiftInput = input.length === 1 &&
|
|
143
|
-
input === input.toUpperCase() &&
|
|
144
|
-
input.toLowerCase() === mainKey;
|
|
145
|
-
if (hasCtrl && !key.ctrl)
|
|
146
|
-
return false;
|
|
147
|
-
if (hasMeta && !key.meta)
|
|
148
|
-
return false;
|
|
149
|
-
if (hasShift && !key.shift && !uppercaseShiftInput)
|
|
150
|
-
return false;
|
|
151
|
-
// Block lowercase-only bindings when shift is active or the input is
|
|
152
|
-
// an uppercase letter (which implies Shift was held).
|
|
153
|
-
// Example: the 'p' (Plugins) binding must not fire when the user
|
|
154
|
-
// presses Shift+P, which should only trigger 'shift+p' (Playlists).
|
|
155
|
-
// Note: `input !== input.toLowerCase()` is true only for uppercase
|
|
156
|
-
// alphabetical characters, avoiding false positives on symbols/digits.
|
|
157
|
-
if (!hasShift &&
|
|
158
|
-
(key.shift ||
|
|
159
|
-
(input.length === 1 && input !== input.toLowerCase())))
|
|
160
|
-
return false;
|
|
161
|
-
// Check the actual key
|
|
162
|
-
if (mainKey === 'up' && key.upArrow)
|
|
163
|
-
return true;
|
|
164
|
-
if (mainKey === 'down' && key.downArrow)
|
|
165
|
-
return true;
|
|
166
|
-
if (mainKey === 'left' && key.leftArrow)
|
|
167
|
-
return true;
|
|
168
|
-
if (mainKey === 'right' && key.rightArrow)
|
|
169
|
-
return true;
|
|
170
|
-
// Handle '=' and '+' specially (+ is shift+=)
|
|
171
|
-
if (mainKey === '=' && input === '=')
|
|
172
|
-
return true;
|
|
173
|
-
if (mainKey === '+' && input === '+')
|
|
174
|
-
return true;
|
|
175
|
-
if (mainKey === '+' && key.shift && input === '=')
|
|
176
|
-
return true; // shift+= produces '+'
|
|
177
|
-
return input.toLowerCase() === mainKey && !key.ctrl && !key.meta;
|
|
178
|
-
})();
|
|
179
|
-
if (isMatch) {
|
|
180
|
-
handler();
|
|
181
|
-
// We don't break here because multiple handlers might want to react
|
|
182
|
-
// but usually only one does.
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
const KeyboardBlockContext = createContext(null);
|
|
4
|
-
export function KeyboardBlockProvider({ children }) {
|
|
5
|
-
const [blockCount, setBlockCount] = useState(0);
|
|
6
|
-
const increment = useCallback(() => {
|
|
7
|
-
setBlockCount(prev => prev + 1);
|
|
8
|
-
}, []);
|
|
9
|
-
const decrement = useCallback(() => {
|
|
10
|
-
setBlockCount(prev => Math.max(0, prev - 1));
|
|
11
|
-
}, []);
|
|
12
|
-
const value = useMemo(() => ({ blockCount, increment, decrement }), [blockCount, increment, decrement]);
|
|
13
|
-
return (_jsx(KeyboardBlockContext.Provider, { value: value, children: children }));
|
|
14
|
-
}
|
|
15
|
-
export function useKeyboardBlockContext() {
|
|
16
|
-
const context = useContext(KeyboardBlockContext);
|
|
17
|
-
if (!context) {
|
|
18
|
-
throw new Error('useKeyboardBlockContext must be used within KeyboardBlockProvider');
|
|
19
|
-
}
|
|
20
|
-
return context;
|
|
21
|
-
}
|
|
22
|
-
export function useKeyboardBlocker(shouldBlock) {
|
|
23
|
-
const { increment, decrement } = useKeyboardBlockContext();
|
|
24
|
-
const blockedRef = useRef(false);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (shouldBlock && !blockedRef.current) {
|
|
27
|
-
increment();
|
|
28
|
-
blockedRef.current = true;
|
|
29
|
-
}
|
|
30
|
-
else if (!shouldBlock && blockedRef.current) {
|
|
31
|
-
decrement();
|
|
32
|
-
blockedRef.current = false;
|
|
33
|
-
}
|
|
34
|
-
return () => {
|
|
35
|
-
if (blockedRef.current) {
|
|
36
|
-
decrement();
|
|
37
|
-
blockedRef.current = false;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
}, [shouldBlock, increment, decrement]);
|
|
41
|
-
}
|
|
42
|
-
export function useIsKeyboardBlocked() {
|
|
43
|
-
const { blockCount } = useKeyboardBlockContext();
|
|
44
|
-
return blockCount > 0;
|
|
45
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
if (options?.clearQueue) {
|
|
9
|
-
// When clearing the queue, always dispatch fresh play commands rather
|
|
10
|
-
// than relying on the stale queue state captured in this closure.
|
|
11
|
-
// This fixes a bug where a track already in the queue wouldn't replay
|
|
12
|
-
// after clearQueue because SET_QUEUE_POSITION would be dispatched
|
|
13
|
-
// against the (now-empty) queue.
|
|
14
|
-
dispatch({ category: 'CLEAR_QUEUE' });
|
|
15
|
-
dispatch({ category: 'ADD_TO_QUEUE', track });
|
|
16
|
-
dispatch({ category: 'PLAY', track });
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
// Add to queue if not already there
|
|
20
|
-
const isInQueue = state.queue.some(t => t.videoId === track.videoId);
|
|
21
|
-
if (!isInQueue) {
|
|
22
|
-
dispatch({ category: 'ADD_TO_QUEUE', track });
|
|
23
|
-
}
|
|
24
|
-
// Find position and play
|
|
25
|
-
const position = state.queue.findIndex(t => t.videoId === track.videoId);
|
|
26
|
-
if (position >= 0) {
|
|
27
|
-
dispatch({ category: 'SET_QUEUE_POSITION', position });
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
dispatch({ category: 'PLAY', track });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Add to history
|
|
34
|
-
const config = getConfigService();
|
|
35
|
-
config.addToHistory(track.videoId);
|
|
36
|
-
}, [state.queue, dispatch]);
|
|
37
|
-
return {
|
|
38
|
-
...playerStore,
|
|
39
|
-
state,
|
|
40
|
-
dispatch,
|
|
41
|
-
play,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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, tracks = []) => {
|
|
11
|
-
const newPlaylist = {
|
|
12
|
-
playlistId: Date.now().toString(),
|
|
13
|
-
name,
|
|
14
|
-
tracks: tracks.map(track => ({ ...track })),
|
|
15
|
-
};
|
|
16
|
-
const updatedPlaylists = [...playlists, newPlaylist];
|
|
17
|
-
setPlaylists(updatedPlaylists);
|
|
18
|
-
configService.set('playlists', updatedPlaylists);
|
|
19
|
-
return newPlaylist;
|
|
20
|
-
}, [playlists, configService]);
|
|
21
|
-
const deletePlaylist = useCallback((playlistId) => {
|
|
22
|
-
const updatedPlaylists = playlists.filter(p => p.playlistId !== playlistId);
|
|
23
|
-
setPlaylists(updatedPlaylists);
|
|
24
|
-
configService.set('playlists', updatedPlaylists);
|
|
25
|
-
}, [playlists, configService]);
|
|
26
|
-
const addTrackToPlaylist = useCallback((playlistId, track, force = false) => {
|
|
27
|
-
const playlistIndex = playlists.findIndex(p => p.playlistId === playlistId);
|
|
28
|
-
if (playlistIndex === -1)
|
|
29
|
-
return 'added';
|
|
30
|
-
const playlist = playlists[playlistIndex];
|
|
31
|
-
const isDuplicate = playlist.tracks.some(t => t.videoId === track.videoId);
|
|
32
|
-
if (isDuplicate && !force) {
|
|
33
|
-
return 'duplicate';
|
|
34
|
-
}
|
|
35
|
-
const updatedPlaylists = [...playlists];
|
|
36
|
-
updatedPlaylists[playlistIndex].tracks.push(track);
|
|
37
|
-
setPlaylists(updatedPlaylists);
|
|
38
|
-
configService.set('playlists', updatedPlaylists);
|
|
39
|
-
return 'added';
|
|
40
|
-
}, [playlists, configService]);
|
|
41
|
-
const renamePlaylist = useCallback((playlistId, newName) => {
|
|
42
|
-
const updatedPlaylists = playlists.map(playlist => playlist.playlistId === playlistId
|
|
43
|
-
? { ...playlist, name: newName }
|
|
44
|
-
: playlist);
|
|
45
|
-
setPlaylists(updatedPlaylists);
|
|
46
|
-
configService.set('playlists', updatedPlaylists);
|
|
47
|
-
}, [playlists, configService]);
|
|
48
|
-
const removeTrackFromPlaylist = useCallback((playlistId, trackIndex) => {
|
|
49
|
-
const playlistIndex = playlists.findIndex(p => p.playlistId === playlistId);
|
|
50
|
-
if (playlistIndex === -1)
|
|
51
|
-
return;
|
|
52
|
-
const updatedPlaylists = [...playlists];
|
|
53
|
-
updatedPlaylists[playlistIndex].tracks.splice(trackIndex, 1);
|
|
54
|
-
setPlaylists(updatedPlaylists);
|
|
55
|
-
configService.set('playlists', updatedPlaylists);
|
|
56
|
-
}, [playlists, configService]);
|
|
57
|
-
return {
|
|
58
|
-
playlists,
|
|
59
|
-
createPlaylist,
|
|
60
|
-
deletePlaylist,
|
|
61
|
-
renamePlaylist,
|
|
62
|
-
addTrackToPlaylist,
|
|
63
|
-
removeTrackFromPlaylist,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Hook for managing the sleep timer
|
|
2
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
-
import { getSleepTimerService, SLEEP_TIMER_PRESETS, } from "../services/sleep-timer/sleep-timer.service.js";
|
|
4
|
-
import { usePlayer } from "./usePlayer.js";
|
|
5
|
-
export function useSleepTimer() {
|
|
6
|
-
const { pause } = usePlayer();
|
|
7
|
-
const timerService = getSleepTimerService();
|
|
8
|
-
const [remainingSeconds, setRemainingSeconds] = useState(null);
|
|
9
|
-
const [activeMinutes, setActiveMinutes] = useState(null);
|
|
10
|
-
// Poll remaining time every second when active
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (!timerService.isActive())
|
|
13
|
-
return;
|
|
14
|
-
const interval = setInterval(() => {
|
|
15
|
-
const remaining = timerService.getRemainingSeconds();
|
|
16
|
-
setRemainingSeconds(remaining);
|
|
17
|
-
if (remaining === 0) {
|
|
18
|
-
setActiveMinutes(null);
|
|
19
|
-
clearInterval(interval);
|
|
20
|
-
}
|
|
21
|
-
}, 1000);
|
|
22
|
-
return () => {
|
|
23
|
-
clearInterval(interval);
|
|
24
|
-
};
|
|
25
|
-
}, [activeMinutes, timerService]);
|
|
26
|
-
const startTimer = useCallback((minutes) => {
|
|
27
|
-
setActiveMinutes(minutes);
|
|
28
|
-
setRemainingSeconds(minutes * 60);
|
|
29
|
-
timerService.start(minutes, () => {
|
|
30
|
-
pause();
|
|
31
|
-
setActiveMinutes(null);
|
|
32
|
-
setRemainingSeconds(null);
|
|
33
|
-
});
|
|
34
|
-
}, [timerService, pause]);
|
|
35
|
-
const cancelTimer = useCallback(() => {
|
|
36
|
-
timerService.cancel();
|
|
37
|
-
setActiveMinutes(null);
|
|
38
|
-
setRemainingSeconds(null);
|
|
39
|
-
}, [timerService]);
|
|
40
|
-
return {
|
|
41
|
-
isActive: timerService.isActive(),
|
|
42
|
-
activeMinutes,
|
|
43
|
-
remainingSeconds,
|
|
44
|
-
startTimer,
|
|
45
|
-
cancelTimer,
|
|
46
|
-
presets: SLEEP_TIMER_PRESETS,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
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
|
-
}
|