@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli.js.map +6 -6
  3. package/dist/youtube-music-cli +0 -0
  4. package/package.json +1 -1
  5. package/dist/eslint.config.js +0 -55
  6. package/dist/package.json +0 -120
  7. package/dist/scripts/build-cli.js +0 -46
  8. package/dist/source/app.js +0 -17
  9. package/dist/source/cli.js +0 -504
  10. package/dist/source/components/common/ErrorBoundary.js +0 -22
  11. package/dist/source/components/common/Help.js +0 -18
  12. package/dist/source/components/common/ShortcutsBar.js +0 -89
  13. package/dist/source/components/config/ConfigLayout.js +0 -84
  14. package/dist/source/components/config/KeybindingsLayout.js +0 -107
  15. package/dist/source/components/export/ExportLayout.js +0 -111
  16. package/dist/source/components/import/ImportLayout.js +0 -119
  17. package/dist/source/components/import/ImportProgress.js +0 -73
  18. package/dist/source/components/layouts/ExploreLayout.js +0 -72
  19. package/dist/source/components/layouts/HistoryLayout.js +0 -37
  20. package/dist/source/components/layouts/LyricsLayout.js +0 -89
  21. package/dist/source/components/layouts/MainLayout.js +0 -190
  22. package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
  23. package/dist/source/components/layouts/PlayerLayout.js +0 -9
  24. package/dist/source/components/layouts/PluginsLayout.js +0 -77
  25. package/dist/source/components/layouts/SearchLayout.js +0 -193
  26. package/dist/source/components/layouts/TrendingLayout.js +0 -59
  27. package/dist/source/components/player/NowPlaying.js +0 -45
  28. package/dist/source/components/player/PlayerControls.js +0 -83
  29. package/dist/source/components/player/ProgressBar.js +0 -19
  30. package/dist/source/components/player/QueueList.js +0 -36
  31. package/dist/source/components/player/Suggestions.js +0 -50
  32. package/dist/source/components/playlist/PlaylistList.js +0 -138
  33. package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
  34. package/dist/source/components/plugins/PluginsAvailable.js +0 -55
  35. package/dist/source/components/plugins/PluginsList.js +0 -18
  36. package/dist/source/components/search/SearchBar.js +0 -55
  37. package/dist/source/components/search/SearchHistory.js +0 -35
  38. package/dist/source/components/search/SearchResults.js +0 -280
  39. package/dist/source/components/settings/Settings.js +0 -211
  40. package/dist/source/components/theme/ThemeSwitcher.js +0 -11
  41. package/dist/source/config/themes.config.js +0 -123
  42. package/dist/source/contexts/theme.context.js +0 -29
  43. package/dist/source/hooks/useKeyboard.js +0 -188
  44. package/dist/source/hooks/useKeyboardBlocker.js +0 -45
  45. package/dist/source/hooks/useNavigation.js +0 -5
  46. package/dist/source/hooks/usePlayer.js +0 -43
  47. package/dist/source/hooks/usePlaylist.js +0 -65
  48. package/dist/source/hooks/useSearch.js +0 -76
  49. package/dist/source/hooks/useSleepTimer.js +0 -48
  50. package/dist/source/hooks/useTerminalSize.js +0 -24
  51. package/dist/source/hooks/useTheme.js +0 -5
  52. package/dist/source/hooks/useYouTubeMusic.js +0 -112
  53. package/dist/source/main.js +0 -127
  54. package/dist/source/services/cache/cache.service.js +0 -67
  55. package/dist/source/services/completions/completions.service.js +0 -313
  56. package/dist/source/services/config/config.service.js +0 -191
  57. package/dist/source/services/discord/discord-rpc.service.js +0 -95
  58. package/dist/source/services/download/download.service.js +0 -350
  59. package/dist/source/services/export/export.service.js +0 -131
  60. package/dist/source/services/history/history.service.js +0 -83
  61. package/dist/source/services/import/import.service.js +0 -272
  62. package/dist/source/services/import/spotify.service.js +0 -171
  63. package/dist/source/services/import/track-matcher.service.js +0 -271
  64. package/dist/source/services/import/youtube-import.service.js +0 -84
  65. package/dist/source/services/logger/logger.service.js +0 -52
  66. package/dist/source/services/lyrics/lyrics.service.js +0 -93
  67. package/dist/source/services/mpris/mpris.service.js +0 -78
  68. package/dist/source/services/notification/notification.service.js +0 -57
  69. package/dist/source/services/player/dependency-check.service.js +0 -140
  70. package/dist/source/services/player/player.service.js +0 -478
  71. package/dist/source/services/player-state/player-state.service.js +0 -123
  72. package/dist/source/services/plugin/plugin-audio-api.js +0 -36
  73. package/dist/source/services/plugin/plugin-context.js +0 -256
  74. package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
  75. package/dist/source/services/plugin/plugin-installer.service.js +0 -248
  76. package/dist/source/services/plugin/plugin-loader.service.js +0 -161
  77. package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
  78. package/dist/source/services/plugin/plugin-registry.service.js +0 -215
  79. package/dist/source/services/plugin/plugin-ui-api.js +0 -46
  80. package/dist/source/services/plugin/plugin-updater.service.js +0 -206
  81. package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
  82. package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
  83. package/dist/source/services/version-check/version-check.service.js +0 -121
  84. package/dist/source/services/web/static-file.service.js +0 -185
  85. package/dist/source/services/web/web-server-manager.js +0 -507
  86. package/dist/source/services/web/web-streaming.service.js +0 -292
  87. package/dist/source/services/web/websocket.server.js +0 -267
  88. package/dist/source/services/youtube-music/api.js +0 -649
  89. package/dist/source/services/youtube-music/search.service.js +0 -38
  90. package/dist/source/stores/history.store.js +0 -64
  91. package/dist/source/stores/navigation.store.js +0 -90
  92. package/dist/source/stores/player.store.js +0 -789
  93. package/dist/source/stores/plugins.store.js +0 -177
  94. package/dist/source/types/actions.js +0 -1
  95. package/dist/source/types/cli.types.js +0 -1
  96. package/dist/source/types/config.types.js +0 -1
  97. package/dist/source/types/history.types.js +0 -1
  98. package/dist/source/types/import.types.js +0 -2
  99. package/dist/source/types/keyboard.types.js +0 -1
  100. package/dist/source/types/navigation.types.js +0 -1
  101. package/dist/source/types/player.types.js +0 -1
  102. package/dist/source/types/playlist.types.js +0 -1
  103. package/dist/source/types/plugin.types.js +0 -1
  104. package/dist/source/types/theme.types.js +0 -1
  105. package/dist/source/types/web.types.js +0 -2
  106. package/dist/source/types/youtube-music.types.js +0 -1
  107. package/dist/source/types/youtubei.types.js +0 -3
  108. package/dist/source/utils/constants.js +0 -135
  109. package/dist/source/utils/format.js +0 -24
  110. package/dist/source/utils/icons.js +0 -28
  111. package/dist/source/utils/search-filters.js +0 -100
@@ -1,45 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Now playing component
3
- import { Box, Text } from 'ink';
4
- import { usePlayer } from "../../hooks/usePlayer.js";
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- import { formatTime } from "../../utils/format.js";
7
- import { useTerminalSize } from "../../hooks/useTerminalSize.js";
8
- import { getSleepTimerService } from "../../services/sleep-timer/sleep-timer.service.js";
9
- import { useState, useEffect } from 'react';
10
- import { ICONS } from "../../utils/icons.js";
11
- export default function NowPlaying() {
12
- const { theme } = useTheme();
13
- const { state: playerState } = usePlayer();
14
- const { columns } = useTerminalSize();
15
- const sleepTimer = getSleepTimerService();
16
- const [sleepRemaining, setSleepRemaining] = useState(null);
17
- // Poll sleep timer remaining every second
18
- useEffect(() => {
19
- if (!sleepTimer.isActive()) {
20
- return;
21
- }
22
- const interval = setInterval(() => {
23
- const remaining = sleepTimer.getRemainingSeconds();
24
- setSleepRemaining(remaining);
25
- if (remaining === null || remaining === 0) {
26
- clearInterval(interval);
27
- }
28
- }, 1000);
29
- return () => {
30
- clearInterval(interval);
31
- };
32
- }, [sleepTimer]);
33
- if (!playerState.currentTrack) {
34
- return (_jsx(Box, { borderStyle: "round", borderColor: theme.colors.dim, paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "No track playing" }) }));
35
- }
36
- const track = playerState.currentTrack;
37
- const artists = track.artists?.map(a => a.name).join(', ') || 'Unknown Artist';
38
- // Clamp progress to valid range
39
- const progress = Math.max(0, Math.min(playerState.progress, playerState.duration || 0));
40
- const duration = playerState.duration || 0;
41
- const percentage = duration > 0 ? Math.min(100, Math.floor((progress / duration) * 100)) : 0;
42
- const barWidth = Math.max(10, columns - 8);
43
- const filledWidth = duration > 0 ? Math.floor((progress / duration) * barWidth) : 0;
44
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.primary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: track.title }), _jsx(Text, { color: theme.colors.dim, children: " \u2022 " }), _jsx(Text, { color: theme.colors.secondary, children: artists })] }), track.album && _jsx(Text, { color: theme.colors.dim, children: track.album.name }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: '█'.repeat(Math.min(filledWidth, barWidth)) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(Math.max(0, barWidth - filledWidth)) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsxs(Text, { color: theme.colors.dim, children: [" / ", formatTime(duration), " "] }), _jsxs(Text, { color: theme.colors.dim, children: ["[", percentage, "%]"] }), playerState.isLoading && (_jsx(Text, { color: theme.colors.accent, children: " Loading..." })), !playerState.isPlaying && progress > 0 && (_jsxs(Text, { color: theme.colors.dim, children: [" ", ICONS.PAUSE] })), playerState.shuffle && (_jsxs(Text, { color: theme.colors.primary, children: [" ", ICONS.SHUFFLE] })), sleepRemaining !== null && (_jsxs(Text, { color: theme.colors.warning, children: [' ', "\u23FE ", formatTime(sleepRemaining)] }))] }), playerState.error && (_jsx(Text, { color: theme.colors.error, children: playerState.error }))] }));
45
- }
@@ -1,83 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Player controls component
3
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
4
- import { KEYBINDINGS } from "../../utils/constants.js";
5
- import { usePlayer } from "../../hooks/usePlayer.js";
6
- import { useTheme } from "../../hooks/useTheme.js";
7
- import { Box, Text } from 'ink';
8
- import { useEffect, useState } from 'react';
9
- import { logger } from "../../services/logger/logger.service.js";
10
- import { ICONS } from "../../utils/icons.js";
11
- import { getConfigService } from "../../services/config/config.service.js";
12
- let mountCount = 0;
13
- const CROSSFADE_PRESETS = [0, 1, 2, 3, 5];
14
- const EQUALIZER_PRESETS = [
15
- 'flat',
16
- 'bass_boost',
17
- 'vocal',
18
- 'bright',
19
- 'warm',
20
- ];
21
- const formatEqualizerLabel = (preset) => preset
22
- .split('_')
23
- .map(segment => `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`)
24
- .join(' ');
25
- export default function PlayerControls() {
26
- const instanceId = ++mountCount;
27
- useEffect(() => {
28
- logger.debug('PlayerControls', 'Component mounted', { instanceId });
29
- return () => {
30
- logger.debug('PlayerControls', 'Component unmounted', { instanceId });
31
- };
32
- }, [instanceId]);
33
- const { theme } = useTheme();
34
- const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, speedUp, speedDown, toggleShuffle, } = usePlayer();
35
- const config = getConfigService();
36
- const [gaplessPlayback, setGaplessPlayback] = useState(config.get('gaplessPlayback') ?? true);
37
- const [crossfadeDuration, setCrossfadeDuration] = useState(config.get('crossfadeDuration') ?? 0);
38
- const [equalizerPreset, setEqualizerPreset] = useState(config.get('equalizerPreset') ?? 'flat');
39
- // DEBUG: Log when callbacks change (detect instability)
40
- useEffect(() => {
41
- // Temporarily output to stderr to debug without triggering Ink re-render
42
- process.stderr.write(`[PlayerControls] volumeUp callback: ${typeof volumeUp}\n`);
43
- }, [volumeUp, instanceId]);
44
- const handlePlayPause = () => {
45
- if (playerState.isPlaying) {
46
- pause();
47
- }
48
- else {
49
- resume();
50
- }
51
- };
52
- const toggleGaplessPlayback = () => {
53
- const next = !gaplessPlayback;
54
- setGaplessPlayback(next);
55
- config.set('gaplessPlayback', next);
56
- };
57
- const cycleCrossfadeDuration = () => {
58
- const currentIndex = CROSSFADE_PRESETS.indexOf(crossfadeDuration);
59
- const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % CROSSFADE_PRESETS.length;
60
- const next = CROSSFADE_PRESETS[nextIndex] ?? 0;
61
- setCrossfadeDuration(next);
62
- config.set('crossfadeDuration', next);
63
- };
64
- const cycleEqualizerPreset = () => {
65
- const currentIndex = EQUALIZER_PRESETS.indexOf(equalizerPreset);
66
- const next = EQUALIZER_PRESETS[(currentIndex + 1) % EQUALIZER_PRESETS.length];
67
- setEqualizerPreset(next);
68
- config.set('equalizerPreset', next);
69
- };
70
- // Keyboard bindings
71
- useKeyBinding(KEYBINDINGS.PLAY_PAUSE, handlePlayPause);
72
- useKeyBinding(KEYBINDINGS.NEXT, next);
73
- useKeyBinding(KEYBINDINGS.PREVIOUS, previous);
74
- useKeyBinding(KEYBINDINGS.VOLUME_UP, volumeUp);
75
- useKeyBinding(KEYBINDINGS.VOLUME_DOWN, volumeDown);
76
- useKeyBinding(KEYBINDINGS.SPEED_UP, speedUp);
77
- useKeyBinding(KEYBINDINGS.SPEED_DOWN, speedDown);
78
- useKeyBinding(KEYBINDINGS.SHUFFLE, toggleShuffle);
79
- useKeyBinding(KEYBINDINGS.GAPLESS_TOGGLE, toggleGaplessPlayback);
80
- useKeyBinding(KEYBINDINGS.CROSSFADE_CYCLE, cycleCrossfadeDuration);
81
- useKeyBinding(KEYBINDINGS.EQUALIZER_CYCLE, cycleEqualizerPreset);
82
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 2, borderStyle: "classic", borderColor: theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2190 / b" }), "] Prev"] }), _jsx(Text, { color: theme.colors.primary, children: playerState.isPlaying ? (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Pause"] })) : (_jsxs(Text, { children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Space" }), "] Play"] })) }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "\u2192 / n" }), "] Next"] }), _jsxs(Text, { color: theme.colors.text, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "+/-" }), "] Vol: ", playerState.volume, "%"] }), _jsxs(Text, { color: playerState.shuffle ? theme.colors.primary : theme.colors.dim, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "Shift+S" }), "]", ' ', playerState.shuffle ? `${ICONS.SHUFFLE} ON` : `${ICONS.SHUFFLE} OFF`] }), (playerState.speed ?? 1.0) !== 1.0 && (_jsxs(Text, { color: theme.colors.accent, children: ["[", _jsx(Text, { color: theme.colors.dim, children: "<>" }), "]", ' ', (playerState.speed ?? 1.0).toFixed(2), "x"] }))] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 2, gap: 2, children: [_jsxs(Text, { color: gaplessPlayback ? theme.colors.primary : theme.colors.dim, children: ["Gapless: ", gaplessPlayback ? 'ON' : 'OFF'] }), _jsxs(Text, { color: theme.colors.text, children: ["Crossfade: ", crossfadeDuration === 0 ? 'Off' : `${crossfadeDuration}s`] }), _jsxs(Text, { color: theme.colors.text, children: ["Equalizer: ", formatEqualizerLabel(equalizerPreset)] })] })] }));
83
- }
@@ -1,19 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Progress bar component
3
- import { Box, Text } from 'ink';
4
- import { useTheme } from "../../hooks/useTheme.js";
5
- import { usePlayer } from "../../hooks/usePlayer.js";
6
- import { formatTime } from "../../utils/format.js";
7
- export default function ProgressBar() {
8
- const { theme } = useTheme();
9
- const { state: playerState } = usePlayer();
10
- if (!playerState.currentTrack || !playerState.duration) {
11
- return null;
12
- }
13
- // Clamp values to valid range
14
- const progress = Math.max(0, Math.min(playerState.progress, playerState.duration));
15
- const duration = playerState.duration;
16
- const percentage = duration > 0 ? Math.min(100, Math.floor((progress / duration) * 100)) : 0;
17
- const barWidth = Math.min(20, Math.floor(percentage / 5));
18
- return (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, children: formatTime(progress) }), _jsx(Text, { color: theme.colors.dim, children: "/" }), _jsx(Text, { color: theme.colors.text, children: formatTime(duration) }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.colors.primary, children: '█'.repeat(barWidth) }), _jsx(Text, { color: theme.colors.dim, children: '░'.repeat(20 - barWidth) }), _jsxs(Text, { color: theme.colors.dim, children: [" ", percentage, "%"] })] }));
19
- }
@@ -1,36 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- // Queue management component
3
- import { useState } from 'react';
4
- import React from 'react';
5
- import { Box, Text } from 'ink';
6
- import { useTheme } from "../../hooks/useTheme.js";
7
- import { usePlayer } from "../../hooks/usePlayer.js";
8
- import { truncate } from "../../utils/format.js";
9
- import { useTerminalSize } from "../../hooks/useTerminalSize.js";
10
- function QueueList() {
11
- const { theme } = useTheme();
12
- const { state: playerState } = usePlayer();
13
- const { columns } = useTerminalSize();
14
- const [selectedIndex, _setSelectedIndex] = useState(0);
15
- // Calculate responsive truncation
16
- const getTruncateLength = (baseLength) => {
17
- const scale = Math.min(1, columns / 100);
18
- return Math.max(20, Math.floor(baseLength * scale));
19
- };
20
- if (playerState.queue.length === 0) {
21
- return null;
22
- }
23
- // Show only next 5 tracks
24
- const visibleQueue = playerState.queue.slice(playerState.queuePosition + 1, playerState.queuePosition + 6);
25
- if (visibleQueue.length === 0) {
26
- return null;
27
- }
28
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.dim, children: ["Up next (", playerState.queue.length - playerState.queuePosition - 1, ' ', "tracks)"] }), visibleQueue.map((track, idx) => {
29
- const index = playerState.queuePosition + 1 + idx;
30
- const isSelected = index === selectedIndex;
31
- const artists = track.artists?.map(a => a.name).join(', ') || 'Unknown';
32
- const title = truncate(track.title, getTruncateLength(40));
33
- return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.dim, children: [index + 1, ". "] }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, children: title }), _jsxs(Text, { color: theme.colors.dim, children: [" \u2022 ", artists] })] }, track.videoId));
34
- })] }));
35
- }
36
- export default React.memo(QueueList);
@@ -1,50 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Suggestions component
3
- import { useEffect, useState, useCallback } from 'react';
4
- import { Box, Text } from 'ink';
5
- import { useYouTubeMusic } from "../../hooks/useYouTubeMusic.js";
6
- import { usePlayer } from "../../hooks/usePlayer.js";
7
- import { useTheme } from "../../hooks/useTheme.js";
8
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
9
- import { KEYBINDINGS } from "../../utils/constants.js";
10
- import { truncate } from "../../utils/format.js";
11
- export default function Suggestions() {
12
- const { theme } = useTheme();
13
- const { state: playerState, play } = usePlayer();
14
- const { getSuggestions, isLoading } = useYouTubeMusic();
15
- const [suggestions, setSuggestions] = useState([]);
16
- const [selectedIndex, setSelectedIndex] = useState(0);
17
- useEffect(() => {
18
- if (playerState.currentTrack?.videoId) {
19
- getSuggestions(playerState.currentTrack.videoId).then(tracks => {
20
- setSuggestions(tracks);
21
- setSelectedIndex(0);
22
- });
23
- }
24
- }, [playerState.currentTrack?.videoId, getSuggestions]);
25
- const navigateUp = useCallback(() => {
26
- setSelectedIndex(prev => Math.max(0, prev - 1));
27
- }, []);
28
- const navigateDown = useCallback(() => {
29
- setSelectedIndex(prev => Math.min(suggestions.length - 1, prev + 1));
30
- }, [suggestions.length]);
31
- const playSelected = useCallback(() => {
32
- const track = suggestions[selectedIndex];
33
- if (track) {
34
- play(track);
35
- }
36
- }, [selectedIndex, suggestions, play]);
37
- useKeyBinding(KEYBINDINGS.UP, navigateUp);
38
- useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
39
- useKeyBinding(KEYBINDINGS.SELECT, playSelected);
40
- if (isLoading) {
41
- return _jsx(Text, { color: theme.colors.accent, children: "Loading suggestions..." });
42
- }
43
- if (suggestions.length === 0) {
44
- return _jsx(Text, { color: theme.colors.dim, children: "No suggestions available" });
45
- }
46
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.primary, children: ["Suggestions based on: ", playerState.currentTrack?.title] }), suggestions.map((track, index) => {
47
- const isSelected = index === selectedIndex;
48
- return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: isSelected ? theme.colors.primary : undefined, color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: [index + 1, ". ", truncate(track.title, 40), " -", ' ', track.artists?.map(a => a.name).join(', ')] }) }, track.videoId));
49
- }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Arrows to navigate, Enter to play, Esc to go back" }) })] }));
50
- }
@@ -1,138 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Playlist list component
3
- import { Box, Text } from 'ink';
4
- import TextInput from 'ink-text-input';
5
- import { useCallback, useState } from 'react';
6
- import { useNavigation } from "../../hooks/useNavigation.js";
7
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
8
- import { usePlayer } from "../../hooks/usePlayer.js";
9
- import { usePlaylist } from "../../hooks/usePlaylist.js";
10
- import { useTheme } from "../../hooks/useTheme.js";
11
- import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
12
- import { KEYBINDINGS } from "../../utils/constants.js";
13
- import { getDownloadService } from "../../services/download/download.service.js";
14
- export default function PlaylistList() {
15
- const { theme } = useTheme();
16
- const { play, setQueue } = usePlayer();
17
- const { dispatch } = useNavigation();
18
- const downloadService = getDownloadService();
19
- const { playlists, createPlaylist, renamePlaylist, deletePlaylist } = usePlaylist();
20
- const [selectedIndex, setSelectedIndex] = useState(0);
21
- const [lastCreated, setLastCreated] = useState(null);
22
- const [renamingPlaylistId, setRenamingPlaylistId] = useState(null);
23
- const [renameValue, setRenameValue] = useState('');
24
- const [downloadStatus, setDownloadStatus] = useState(null);
25
- const [isDownloading, setIsDownloading] = useState(false);
26
- useKeyboardBlocker(renamingPlaylistId !== null);
27
- const handleCreate = useCallback(() => {
28
- const name = `Playlist ${playlists.length + 1}`;
29
- const playlist = createPlaylist(name);
30
- setLastCreated(playlist.name);
31
- setSelectedIndex(playlists.length);
32
- }, [createPlaylist, playlists.length]);
33
- const navigateUp = useCallback(() => {
34
- setSelectedIndex(prev => Math.max(0, prev - 1));
35
- }, []);
36
- const navigateDown = useCallback(() => {
37
- setSelectedIndex(prev => Math.min(playlists.length === 0 ? 0 : playlists.length - 1, prev + 1));
38
- }, [playlists.length]);
39
- const startPlaylist = useCallback(() => {
40
- if (renamingPlaylistId)
41
- return;
42
- const playlist = playlists[selectedIndex];
43
- if (!playlist || playlist.tracks.length === 0)
44
- return;
45
- setQueue([...playlist.tracks]);
46
- const firstTrack = playlist.tracks[0];
47
- if (!firstTrack)
48
- return;
49
- play(firstTrack);
50
- }, [play, playlists, selectedIndex, renamingPlaylistId, setQueue]);
51
- const handleRename = useCallback(() => {
52
- const playlist = playlists[selectedIndex];
53
- if (!playlist)
54
- return;
55
- setRenamingPlaylistId(playlist.playlistId);
56
- setRenameValue(playlist.name);
57
- }, [playlists, selectedIndex]);
58
- const handleRenameSubmit = useCallback((value) => {
59
- if (!renamingPlaylistId)
60
- return;
61
- const trimmedValue = value.trim() || `Playlist ${selectedIndex + 1}`;
62
- renamePlaylist(renamingPlaylistId, trimmedValue);
63
- setRenamingPlaylistId(null);
64
- setRenameValue('');
65
- }, [renamePlaylist, renamingPlaylistId, selectedIndex]);
66
- const handleBack = useCallback(() => {
67
- if (renamingPlaylistId) {
68
- setRenamingPlaylistId(null);
69
- setRenameValue('');
70
- return;
71
- }
72
- dispatch({ category: 'GO_BACK' });
73
- }, [dispatch, renamingPlaylistId]);
74
- const handleDelete = useCallback(() => {
75
- if (renamingPlaylistId)
76
- return;
77
- const playlist = playlists[selectedIndex];
78
- if (!playlist)
79
- return;
80
- deletePlaylist(playlist.playlistId);
81
- setSelectedIndex(prev => Math.max(0, prev - 1));
82
- }, [deletePlaylist, playlists, renamingPlaylistId, selectedIndex]);
83
- const handleDownload = useCallback(async () => {
84
- if (renamingPlaylistId)
85
- return;
86
- if (isDownloading) {
87
- setDownloadStatus('Download already in progress. Please wait.');
88
- return;
89
- }
90
- const playlist = playlists[selectedIndex];
91
- if (!playlist)
92
- return;
93
- const config = downloadService.getConfig();
94
- if (!config.enabled) {
95
- setDownloadStatus('Downloads are disabled. Enable Download Feature in Settings.');
96
- return;
97
- }
98
- const target = downloadService.resolvePlaylistTarget(playlist);
99
- if (target.tracks.length === 0) {
100
- setDownloadStatus(`No tracks to download in "${playlist.name}".`);
101
- return;
102
- }
103
- setDownloadStatus(`Downloading ${target.tracks.length} track(s) from "${playlist.name}"... this can take a few minutes.`);
104
- try {
105
- setIsDownloading(true);
106
- const summary = await downloadService.downloadTracks(target.tracks);
107
- setDownloadStatus(`Downloaded ${summary.downloaded}, skipped ${summary.skipped}, failed ${summary.failed}.`);
108
- }
109
- catch (error) {
110
- setDownloadStatus(error instanceof Error ? error.message : 'Failed to download playlist.');
111
- }
112
- finally {
113
- setIsDownloading(false);
114
- }
115
- }, [
116
- downloadService,
117
- isDownloading,
118
- playlists,
119
- renamingPlaylistId,
120
- selectedIndex,
121
- ]);
122
- useKeyBinding(KEYBINDINGS.UP, navigateUp);
123
- useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
124
- useKeyBinding(KEYBINDINGS.SELECT, startPlaylist);
125
- useKeyBinding(['r'], handleRename);
126
- useKeyBinding(KEYBINDINGS.CREATE_PLAYLIST, handleCreate);
127
- useKeyBinding(KEYBINDINGS.DELETE_PLAYLIST, handleDelete);
128
- useKeyBinding(KEYBINDINGS.BACK, handleBack);
129
- useKeyBinding(KEYBINDINGS.DOWNLOAD, () => {
130
- void handleDownload();
131
- });
132
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Playlists" }) }), playlists.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No playlists yet" })) : (playlists.map((playlist, index) => {
133
- const isSelected = index === selectedIndex;
134
- const isRenaming = renamingPlaylistId === playlist.playlistId && isSelected;
135
- const rowBackground = isSelected ? theme.colors.secondary : undefined;
136
- return (_jsxs(Box, { paddingX: 1, backgroundColor: rowBackground, children: [_jsxs(Text, { color: isSelected ? theme.colors.background : theme.colors.primary, bold: isSelected, children: [index + 1, "."] }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [isRenaming ? (_jsx(TextInput, { value: renameValue, onChange: setRenameValue, onSubmit: handleRenameSubmit, placeholder: "Playlist name", focus: true })) : (_jsx(Text, { color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: playlist.name })), _jsx(Text, { color: theme.colors.dim, children: ` (${playlist.tracks.length} tracks)` })] })] }, playlist.playlistId));
137
- })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.text, children: "Enter" }), " to play |", ' ', _jsx(Text, { color: theme.colors.text, children: "r" }), " rename |", ' ', _jsx(Text, { color: theme.colors.text, children: "c" }), " create |", ' ', _jsx(Text, { color: theme.colors.text, children: "Shift+D" }), " download |", ' ', _jsx(Text, { color: theme.colors.text, children: "D" }), " delete |", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), " back"] }), lastCreated && (_jsxs(Text, { color: theme.colors.accent, children: [" Created ", lastCreated] })), downloadStatus && (_jsx(Text, { color: theme.colors.accent, children: downloadStatus }))] })] }));
138
- }
@@ -1,41 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Plugin install dialog - prompts for plugin name or URL
3
- import { useState, useCallback } from 'react';
4
- import { Box, Text } from 'ink';
5
- import TextInput from 'ink-text-input';
6
- import { useTheme } from "../../hooks/useTheme.js";
7
- import { usePlugins } from "../../stores/plugins.store.js";
8
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
9
- import { KEYBINDINGS } from "../../utils/constants.js";
10
- export default function PluginInstallDialog({ onClose, }) {
11
- const { theme } = useTheme();
12
- const { installPlugin, state } = usePlugins();
13
- const [input, setInput] = useState('');
14
- const [installing, setInstalling] = useState(false);
15
- const [result, setResult] = useState(null);
16
- const handleSubmit = useCallback(async () => {
17
- if (!input.trim() || installing)
18
- return;
19
- setInstalling(true);
20
- setResult(null);
21
- const installResult = await installPlugin(input.trim());
22
- setInstalling(false);
23
- setResult({
24
- success: installResult.success,
25
- message: installResult.success
26
- ? `Successfully installed ${installResult.pluginId}`
27
- : installResult.error || 'Installation failed',
28
- });
29
- if (installResult.success) {
30
- // Close after a brief delay on success
31
- setTimeout(onClose, 1500);
32
- }
33
- }, [input, installing, installPlugin, onClose]);
34
- const handleClose = useCallback(() => {
35
- if (!installing) {
36
- onClose();
37
- }
38
- }, [installing, onClose]);
39
- useKeyBinding(KEYBINDINGS.BACK, handleClose);
40
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Install Plugin" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Enter a plugin name (from default repo) or GitHub URL:" }) }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: theme.colors.text, children: '> ' }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "e.g., adblock or https://github.com/user/plugin" })] }), installing && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.warning, children: "Installing..." }) })), result && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: result.success ? theme.colors.success : theme.colors.error, children: [result.success ? '✓' : '✗', " ", result.message] }) })), state.error && !result && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.error, children: ["Error: ", state.error] }) })), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: theme.colors.dim, children: ["Press ", _jsx(Text, { color: theme.colors.text, children: "Enter" }), " to install,", ' ', _jsx(Text, { color: theme.colors.text, children: "Esc" }), " to cancel"] }) })] }));
41
- }
@@ -1,55 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Available plugins component - displays plugins from the default repo
3
- import { useMemo } from 'react';
4
- import { Box, Text } from 'ink';
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- import { usePlugins } from "../../stores/plugins.store.js";
7
- // Mock available plugins (in production, these would be fetched from the repo)
8
- const AVAILABLE_PLUGINS = [
9
- {
10
- id: 'adblock',
11
- name: 'Adblock',
12
- version: '1.0.0',
13
- description: 'Blocks ads by filtering known ad video IDs',
14
- author: 'involvex',
15
- repository: 'https://github.com/involvex/youtube-music-cli-plugins',
16
- installUrl: 'adblock',
17
- tags: ['audio', 'filter'],
18
- },
19
- {
20
- id: 'now-playing',
21
- name: 'Now Playing',
22
- version: '1.0.0',
23
- description: 'Shows system notifications when track changes',
24
- author: 'involvex',
25
- repository: 'https://github.com/involvex/youtube-music-cli-plugins',
26
- installUrl: 'now-playing',
27
- tags: ['notifications'],
28
- },
29
- {
30
- id: 'lyrics',
31
- name: 'Lyrics',
32
- version: '1.0.0',
33
- description: 'Displays lyrics for the current track',
34
- author: 'involvex',
35
- repository: 'https://github.com/involvex/youtube-music-cli-plugins',
36
- installUrl: 'lyrics',
37
- tags: ['ui', 'lyrics'],
38
- },
39
- ];
40
- export default function PluginsAvailable({ selectedIndex, }) {
41
- const { theme } = useTheme();
42
- const { state } = usePlugins();
43
- // Use useMemo instead of useEffect to avoid setState in effect
44
- const plugins = useMemo(() => {
45
- const installedIds = new Set(state.installedPlugins.map(p => p.manifest.id));
46
- return AVAILABLE_PLUGINS.filter(p => !installedIds.has(p.id));
47
- }, [state.installedPlugins]);
48
- if (plugins.length === 0) {
49
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "All available plugins are already installed." }) }));
50
- }
51
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Available Plugins" }) }), plugins.map((plugin, index) => {
52
- const isSelected = index === selectedIndex;
53
- return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: isSelected ? theme.colors.primary : undefined, color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: [plugin.name, _jsxs(Text, { color: isSelected ? undefined : theme.colors.dim, children: [' ', "v", plugin.version, " - ", plugin.description] })] }) }, plugin.id));
54
- })] }));
55
- }
@@ -1,18 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Plugins list component - displays installed plugins
3
- import { Box, Text } from 'ink';
4
- import { useTheme } from "../../hooks/useTheme.js";
5
- export default function PluginsList({ plugins, selectedIndex, }) {
6
- const { theme } = useTheme();
7
- if (plugins.length === 0) {
8
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "No plugins installed. Press 'i' to install a plugin." }) }));
9
- }
10
- return (_jsx(Box, { flexDirection: "column", children: plugins.map((plugin, index) => {
11
- const isSelected = index === selectedIndex;
12
- const statusIcon = plugin.enabled ? '●' : '○';
13
- const statusColor = plugin.enabled
14
- ? theme.colors.success
15
- : theme.colors.dim;
16
- return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: isSelected ? theme.colors.primary : undefined, color: isSelected ? theme.colors.background : theme.colors.text, bold: isSelected, children: [_jsx(Text, { color: isSelected ? undefined : statusColor, children: statusIcon }), ' ', plugin.manifest.name, _jsxs(Text, { color: isSelected ? undefined : theme.colors.dim, children: [' ', "v", plugin.manifest.version] })] }) }, plugin.manifest.id));
17
- }) }));
18
- }
@@ -1,55 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Search bar component
3
- import { useNavigation } from "../../hooks/useNavigation.js";
4
- import { useState, useCallback } from 'react';
5
- import React from 'react';
6
- import { SEARCH_TYPE } from "../../utils/constants.js";
7
- import { useTheme } from "../../hooks/useTheme.js";
8
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
9
- import { useKeyboardBlocker } from "../../hooks/useKeyboardBlocker.js";
10
- import { Box, Text } from 'ink';
11
- import TextInput from 'ink-text-input';
12
- import { getConfigService } from "../../services/config/config.service.js";
13
- function SearchBar({ onInput, isActive = true }) {
14
- const { theme } = useTheme();
15
- const { state: navState, dispatch } = useNavigation();
16
- const [input, setInput] = useState('');
17
- const config = getConfigService();
18
- const searchTypes = Object.values(SEARCH_TYPE);
19
- // Handle type switching
20
- const cycleType = useCallback(() => {
21
- if (!isActive)
22
- return;
23
- const currentIndex = searchTypes.indexOf(navState.searchType);
24
- const nextIndex = (currentIndex + 1) % searchTypes.length;
25
- const nextType = searchTypes[nextIndex];
26
- if (nextType) {
27
- dispatch({
28
- category: 'SET_SEARCH_CATEGORY',
29
- searchType: nextType,
30
- });
31
- }
32
- }, [navState.searchType, searchTypes, dispatch, isActive]);
33
- // Handle submit via ink-text-input's onSubmit
34
- const handleSubmit = useCallback((value) => {
35
- if (value && isActive) {
36
- config.addToSearchHistory(value);
37
- dispatch({ category: 'SET_SEARCH_QUERY', query: value });
38
- onInput(value);
39
- }
40
- }, [dispatch, onInput, isActive, config]);
41
- // Handle clearing search
42
- const clearSearch = useCallback(() => {
43
- if (isActive) {
44
- setInput('');
45
- onInput('');
46
- }
47
- }, [isActive, onInput]);
48
- useKeyBinding(['tab'], cycleType);
49
- useKeyBinding(['escape'], clearSearch, { bypassBlock: true });
50
- useKeyboardBlocker(isActive);
51
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.dim, children: "Type: " }), searchTypes.map((type, index) => (_jsxs(Text, { color: navState.searchType === type
52
- ? theme.colors.primary
53
- : theme.colors.dim, bold: navState.searchType === type, children: [type, index < searchTypes.length - 1 && ' '] }, type))), _jsx(Text, { color: theme.colors.dim, children: " (Tab to switch)" })] }), isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "Type to search...", focus: isActive })] })), !isActive && (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.primary, children: "Search: " }), _jsx(Text, { color: theme.colors.dim, children: input || 'Type to search...' })] }))] }));
54
- }
55
- export default React.memo(SearchBar);
@@ -1,35 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Search history component
3
- import { useState, useCallback } from 'react';
4
- import { Box, Text } from 'ink';
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- import { useNavigation } from "../../hooks/useNavigation.js";
7
- import { getConfigService } from "../../services/config/config.service.js";
8
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
9
- import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
10
- export default function SearchHistory({ onSelect }) {
11
- const { theme } = useTheme();
12
- const { dispatch } = useNavigation();
13
- const config = getConfigService();
14
- const history = config.getSearchHistory();
15
- const [selectedIndex, setSelectedIndex] = useState(0);
16
- const navigateUp = useCallback(() => {
17
- setSelectedIndex(prev => Math.max(0, prev - 1));
18
- }, []);
19
- const navigateDown = useCallback(() => {
20
- setSelectedIndex(prev => Math.min(history.length - 1, prev + 1));
21
- }, [history.length]);
22
- const handleSelect = useCallback(() => {
23
- const query = history[selectedIndex];
24
- if (query) {
25
- dispatch({ category: 'NAVIGATE', view: VIEW.SEARCH });
26
- onSelect(query);
27
- }
28
- }, [history, selectedIndex, dispatch, onSelect]);
29
- useKeyBinding(KEYBINDINGS.UP, navigateUp);
30
- useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
31
- useKeyBinding(KEYBINDINGS.SELECT, handleSelect);
32
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Search History" }) }), history.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No search history yet" })) : (history.map((query, index) => (_jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === index ? theme.colors.primary : undefined, color: selectedIndex === index
33
- ? theme.colors.background
34
- : theme.colors.text, children: query }) }, index)))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "\u2191\u2193 to navigate \u2022 Enter to search \u2022 Esc to go back" }) })] }));
35
- }