@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,84 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Config screen layout
3
- import { Box, Text } from 'ink';
4
- import { useState, useCallback } from 'react';
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
7
- import { useNavigation } from "../../hooks/useNavigation.js";
8
- import { KEYBINDINGS } from "../../utils/constants.js";
9
- import { getConfigService } from "../../services/config/config.service.js";
10
- export default function ConfigLayout() {
11
- const { theme, setTheme } = useTheme();
12
- const { dispatch } = useNavigation();
13
- const [selectedSection, setSelectedSection] = useState('theme');
14
- const config = getConfigService();
15
- // Navigate sections
16
- const goUp = useCallback(() => {
17
- const sections = ['theme', 'quality', 'volumeStep'];
18
- const currentIndex = sections.indexOf(selectedSection);
19
- if (currentIndex > 0) {
20
- setSelectedSection(sections[currentIndex - 1]);
21
- }
22
- }, [selectedSection]);
23
- const goDown = useCallback(() => {
24
- const sections = ['theme', 'quality', 'volumeStep'];
25
- const currentIndex = sections.indexOf(selectedSection);
26
- if (currentIndex < sections.length - 1) {
27
- setSelectedSection(sections[currentIndex + 1]);
28
- }
29
- }, [selectedSection]);
30
- // Handle Enter key based on selected section
31
- const handleSelect = useCallback(() => {
32
- if (selectedSection === 'theme') {
33
- const themes = ['dark', 'light', 'midnight', 'matrix'];
34
- const currentTheme = theme.name;
35
- const currentIndex = themes.indexOf(currentTheme);
36
- const nextIndex = (currentIndex + 1) % themes.length;
37
- const nextTheme = themes[nextIndex];
38
- setTheme(nextTheme);
39
- config.set('theme', nextTheme);
40
- }
41
- else if (selectedSection === 'quality') {
42
- const qualities = ['low', 'medium', 'high'];
43
- const currentQuality = config.get('streamQuality');
44
- const currentIndex = qualities.indexOf(currentQuality);
45
- const nextIndex = (currentIndex + 1) % qualities.length;
46
- config.set('streamQuality', qualities[nextIndex]);
47
- }
48
- }, [selectedSection, config, theme, setTheme]);
49
- // Change volume step
50
- const increaseVolumeStep = useCallback(() => {
51
- if (selectedSection === 'volumeStep') {
52
- const current = config.get('volume');
53
- if (current < 100) {
54
- config.set('volume', Math.min(100, current + 10));
55
- }
56
- }
57
- }, [selectedSection, config]);
58
- const decreaseVolumeStep = useCallback(() => {
59
- if (selectedSection === 'volumeStep') {
60
- const current = config.get('volume');
61
- if (current > 0) {
62
- config.set('volume', Math.max(0, current - 10));
63
- }
64
- }
65
- }, [selectedSection, config]);
66
- // Go back
67
- const goBack = useCallback(() => {
68
- dispatch({ category: 'GO_BACK' });
69
- }, [dispatch]);
70
- useKeyBinding(KEYBINDINGS.UP, goUp);
71
- useKeyBinding(KEYBINDINGS.DOWN, goDown);
72
- useKeyBinding(KEYBINDINGS.SELECT, handleSelect);
73
- useKeyBinding(KEYBINDINGS.VOLUME_UP, increaseVolumeStep);
74
- useKeyBinding(KEYBINDINGS.VOLUME_DOWN, decreaseVolumeStep);
75
- useKeyBinding(KEYBINDINGS.BACK, goBack);
76
- const currentTheme = theme.name;
77
- const currentQuality = config.get('streamQuality') || 'high';
78
- const currentVolume = config.get('volume') || 70;
79
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Settings" }) }), _jsxs(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedSection === 'theme' ? theme.colors.primary : theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["Theme: ", _jsx(Text, { color: theme.colors.primary, children: currentTheme })] }), selectedSection === 'theme' && (_jsx(Text, { color: theme.colors.dim, children: " (Press Enter to cycle)" }))] }), _jsxs(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedSection === 'quality'
80
- ? theme.colors.primary
81
- : theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["Stream Quality:", ' ', _jsx(Text, { color: theme.colors.primary, children: currentQuality })] }), selectedSection === 'quality' && (_jsx(Text, { color: theme.colors.dim, children: " (Press Enter to cycle)" }))] }), _jsxs(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedSection === 'volumeStep'
82
- ? theme.colors.primary
83
- : theme.colors.dim, children: [_jsxs(Text, { color: theme.colors.text, children: ["Default Volume:", ' ', _jsxs(Text, { color: theme.colors.primary, children: [currentVolume, "%"] })] }), selectedSection === 'volumeStep' && (_jsx(Text, { color: theme.colors.dim, children: " (Press =/- to adjust)" }))] })] }));
84
- }
@@ -1,107 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Custom keybindings editor — shows all actions and their bound keys
3
- import { Box, Text, useInput } from 'ink';
4
- import { useState } from 'react';
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 { KEYBINDINGS } from "../../utils/constants.js";
9
- function buildEntries() {
10
- const config = getConfigService();
11
- return Object.entries(KEYBINDINGS).map(([action, defaultKeys]) => {
12
- const custom = config.getKeybinding(action);
13
- return {
14
- action,
15
- label: action
16
- .toLowerCase()
17
- .replace(/_/g, ' ')
18
- .replace(/\b\w/g, c => c.toUpperCase()),
19
- keys: custom ?? [...defaultKeys],
20
- };
21
- });
22
- }
23
- export default function KeybindingsLayout() {
24
- const { theme } = useTheme();
25
- const { dispatch } = useNavigation();
26
- const [entries, setEntries] = useState(buildEntries);
27
- const [selectedIndex, setSelectedIndex] = useState(0);
28
- const [isCapturing, setIsCapturing] = useState(false);
29
- const [statusMessage, setStatusMessage] = useState('');
30
- useInput((input, key) => {
31
- if (isCapturing) {
32
- // Build key string from the pressed key
33
- const parts = [];
34
- if (key.ctrl)
35
- parts.push('ctrl');
36
- if (key.meta)
37
- parts.push('meta');
38
- if (key.shift)
39
- parts.push('shift');
40
- let keyName = input;
41
- if (key.upArrow)
42
- keyName = 'up';
43
- else if (key.downArrow)
44
- keyName = 'down';
45
- else if (key.leftArrow)
46
- keyName = 'left';
47
- else if (key.rightArrow)
48
- keyName = 'right';
49
- else if (key.return)
50
- keyName = 'enter';
51
- else if (key.tab)
52
- keyName = 'tab';
53
- else if (key.backspace || key.delete)
54
- keyName = 'backspace';
55
- else if (key.escape) {
56
- setIsCapturing(false);
57
- setStatusMessage('Cancelled');
58
- return;
59
- }
60
- if (!keyName || keyName.length === 0)
61
- return;
62
- parts.push(keyName);
63
- const newKey = parts.join('+');
64
- // Persist new binding
65
- const entry = entries[selectedIndex];
66
- if (!entry)
67
- return;
68
- getConfigService().setKeybinding(entry.action, [newKey]);
69
- setEntries(buildEntries());
70
- setIsCapturing(false);
71
- setStatusMessage(`Bound ${entry.action} to "${newKey}"`);
72
- return;
73
- }
74
- if (key.escape) {
75
- dispatch({ category: 'GO_BACK' });
76
- return;
77
- }
78
- if (key.upArrow || input === 'k') {
79
- setSelectedIndex(i => Math.max(0, i - 1));
80
- }
81
- else if (key.downArrow || input === 'j') {
82
- setSelectedIndex(i => Math.min(entries.length - 1, i + 1));
83
- }
84
- else if (key.return) {
85
- setIsCapturing(true);
86
- setStatusMessage('Press any key to bind...');
87
- }
88
- else if (input === 'r') {
89
- // Reset selected binding to default
90
- const entry = entries[selectedIndex];
91
- if (!entry)
92
- return;
93
- const defaultKeys = KEYBINDINGS[entry.action];
94
- if (defaultKeys) {
95
- getConfigService().setKeybinding(entry.action, [
96
- ...defaultKeys,
97
- ]);
98
- setEntries(buildEntries());
99
- setStatusMessage(`Reset ${entry.action} to default`);
100
- }
101
- }
102
- });
103
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: "Custom Keybindings" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.dim, children: "\u2191/\u2193 Navigate | Enter Edit | r Reset | Esc Back" }) }), statusMessage ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.secondary, children: statusMessage }) })) : null, entries.map((entry, index) => {
104
- const isSelected = index === selectedIndex;
105
- return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: isSelected ? '▶ ' : ' ' }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: entry.label.padEnd(25) }), _jsx(Text, { color: theme.colors.secondary, children: entry.keys.join(', ') })] }, entry.action));
106
- }), isCapturing ? (_jsxs(Box, { marginTop: 1, borderStyle: "single", borderColor: theme.colors.secondary, padding: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: ["Press any key combination...", ' '] }), _jsx(Text, { color: theme.colors.dim, children: "(Esc to cancel)" })] })) : null] }));
107
- }
@@ -1,111 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- // Export layout component for playlist export
3
- import { useState, useCallback, useMemo } from 'react';
4
- import { Box, Text } from 'ink';
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- import { useNavigation } from "../../hooks/useNavigation.js";
7
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
8
- import { KEYBINDINGS } from "../../utils/constants.js";
9
- import { getExportService, } from "../../services/export/export.service.js";
10
- import { getConfigService } from "../../services/config/config.service.js";
11
- const FORMATS = [
12
- { key: 'json', label: 'JSON' },
13
- { key: 'm3u8', label: 'M3U8' },
14
- { key: 'both', label: 'Both (JSON + M3U8)' },
15
- ];
16
- export default function ExportLayout() {
17
- const { theme } = useTheme();
18
- const { dispatch } = useNavigation();
19
- const config = getConfigService();
20
- const exportService = getExportService();
21
- const [step, setStep] = useState('format');
22
- const [selectedFormat, setSelectedFormat] = useState(2); // Default to 'both'
23
- const [selectedPlaylist, setSelectedPlaylist] = useState(-1); // -1 means "Export All"
24
- const [results, setResults] = useState([]);
25
- const playlists = useMemo(() => config.get('playlists') || [], [config]);
26
- const goBack = useCallback(() => {
27
- if (step === 'format') {
28
- dispatch({ category: 'GO_BACK' });
29
- }
30
- else if (step === 'playlist') {
31
- setStep('format');
32
- }
33
- else if (step === 'result') {
34
- setStep('format');
35
- setResults([]);
36
- }
37
- }, [step, dispatch]);
38
- const selectFormat = useCallback(() => {
39
- setStep('playlist');
40
- }, []);
41
- const startExport = useCallback(async () => {
42
- setStep('exporting');
43
- const format = FORMATS[selectedFormat].key;
44
- try {
45
- if (selectedPlaylist === -1) {
46
- // Export all playlists
47
- const exportResults = await exportService.exportAllPlaylists(playlists, {
48
- format,
49
- });
50
- setResults(exportResults);
51
- }
52
- else {
53
- // Export single playlist
54
- const result = await exportService.exportPlaylist(playlists[selectedPlaylist], {
55
- format,
56
- });
57
- setResults([result]);
58
- }
59
- }
60
- catch (error) {
61
- setResults([
62
- {
63
- playlistName: 'Error',
64
- success: false,
65
- files: [],
66
- error: error instanceof Error ? error.message : String(error),
67
- },
68
- ]);
69
- }
70
- finally {
71
- setStep('result');
72
- }
73
- }, [selectedFormat, selectedPlaylist, playlists, exportService]);
74
- // Keyboard bindings
75
- useKeyBinding(KEYBINDINGS.UP, () => {
76
- if (step === 'format') {
77
- setSelectedFormat(prev => Math.max(0, prev - 1));
78
- }
79
- else if (step === 'playlist') {
80
- setSelectedPlaylist(prev => Math.max(-1, prev - 1));
81
- }
82
- });
83
- useKeyBinding(KEYBINDINGS.DOWN, () => {
84
- if (step === 'format') {
85
- setSelectedFormat(prev => Math.min(FORMATS.length - 1, prev + 1));
86
- }
87
- else if (step === 'playlist') {
88
- setSelectedPlaylist(prev => Math.min(playlists.length - 1, prev === -1 && playlists.length > 0 ? 0 : prev + 1));
89
- }
90
- });
91
- useKeyBinding(KEYBINDINGS.SELECT, () => {
92
- if (step === 'format')
93
- selectFormat();
94
- else if (step === 'playlist')
95
- startExport();
96
- else if (step === 'result')
97
- goBack();
98
- });
99
- useKeyBinding(KEYBINDINGS.BACK, goBack);
100
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Export Playlists" }) }), step === 'format' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Select export format:" }), FORMATS.map((format, index) => (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: index === selectedFormat ? theme.colors.primary : undefined, color: index === selectedFormat
101
- ? theme.colors.background
102
- : theme.colors.text, bold: index === selectedFormat, children: [index === selectedFormat ? '► ' : ' ', format.label] }) }, format.key))), playlists.length === 0 && (_jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: theme.colors.error, children: "No playlists found. Create or import playlists first." }) }))] })), step === 'playlist' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Select playlist to export:" }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: selectedPlaylist === -1 ? theme.colors.primary : undefined, color: selectedPlaylist === -1
103
- ? theme.colors.background
104
- : theme.colors.text, bold: selectedPlaylist === -1, children: [selectedPlaylist === -1 ? '► ' : ' ', "Export All (", playlists.length, " playlists)"] }) }), playlists.map((playlist, index) => (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: index === selectedPlaylist ? theme.colors.primary : undefined, color: index === selectedPlaylist
105
- ? theme.colors.background
106
- : theme.colors.text, bold: index === selectedPlaylist, children: [index === selectedPlaylist ? '► ' : ' ', playlist.name, " (", playlist.tracks.length, " tracks)"] }) }, playlist.playlistId)))] })), step === 'exporting' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.primary, children: "Exporting..." }), _jsx(Text, { color: theme.colors.dim, children: selectedPlaylist === -1
107
- ? `Exporting ${playlists.length} playlists...`
108
- : `Exporting ${playlists[selectedPlaylist]?.name || 'playlist'}...` })] })), step === 'result' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.success, bold: true, children: "Export completed!" }) }), results.map((result, index) => (_jsx(Box, { flexDirection: "column", gap: 1, paddingX: 1, children: result.success ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.colors.success, children: ["\u2713 ", result.playlistName] }), result.files.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsx(Text, { color: theme.colors.dim, children: "Exported to:" }), result.files.map(file => (_jsxs(Text, { color: theme.colors.primary, children: ["\u2022 ", file] }, file)))] }))] })) : (_jsxs(Text, { color: theme.colors.error, children: ["\u2717 ", result.playlistName, ": ", result.error] })) }, index))), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Press Enter to continue" }) })] })), step !== 'exporting' && step !== 'result' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: step === 'format'
109
- ? '↑↓ to select, Enter to continue, Esc/q to go back'
110
- : '↑↓ to select, Enter to export, Esc to go back' }) }))] }));
111
- }
@@ -1,119 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Import layout component for playlist import
3
- import { useState, useCallback, useEffect } from 'react';
4
- import { Box, Text } from 'ink';
5
- import TextInput from 'ink-text-input';
6
- import { useTheme } from "../../hooks/useTheme.js";
7
- import { useNavigation } from "../../hooks/useNavigation.js";
8
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
9
- import { KEYBINDINGS } from "../../utils/constants.js";
10
- import { getImportService } from "../../services/import/import.service.js";
11
- import ImportProgressComponent from "./ImportProgress.js";
12
- const SOURCES = [
13
- { key: 'spotify', label: 'Spotify' },
14
- { key: 'youtube', label: 'YouTube' },
15
- ];
16
- export default function ImportLayout() {
17
- const { theme } = useTheme();
18
- const { dispatch } = useNavigation();
19
- const importService = getImportService();
20
- const [step, setStep] = useState('source');
21
- const [selectedSource, setSelectedSource] = useState(0);
22
- const [url, setUrl] = useState('');
23
- const [customName, setCustomName] = useState('');
24
- const [progress, setProgress] = useState(null);
25
- const [result, setResult] = useState(null);
26
- const [error, setError] = useState(null);
27
- const goBack = useCallback(() => {
28
- if (step === 'source') {
29
- dispatch({ category: 'GO_BACK' });
30
- }
31
- else if (step === 'url') {
32
- setStep('source');
33
- }
34
- else if (step === 'name') {
35
- setStep('url');
36
- }
37
- else if (step === 'result') {
38
- setStep('source');
39
- setResult(null);
40
- }
41
- }, [step, dispatch]);
42
- const selectSource = useCallback(() => {
43
- setStep('url');
44
- }, []);
45
- const submitUrl = useCallback(() => {
46
- if (url.trim()) {
47
- setStep('name');
48
- }
49
- }, [url]);
50
- const startImport = useCallback(async () => {
51
- setStep('importing');
52
- setError(null);
53
- try {
54
- const unsubscribe = importService.onProgress(prog => {
55
- setProgress(prog);
56
- });
57
- const source = SOURCES[selectedSource].key;
58
- const importResult = await importService.importPlaylist(source, url, customName || undefined);
59
- unsubscribe();
60
- setResult({
61
- playlistName: importResult.playlistName,
62
- matched: importResult.matched,
63
- total: importResult.total,
64
- errors: importResult.errors,
65
- });
66
- setStep('result');
67
- }
68
- catch (err) {
69
- setError(err instanceof Error ? err.message : String(err));
70
- setStep('source');
71
- }
72
- }, [selectedSource, url, customName, importService]);
73
- const submitName = useCallback(() => {
74
- startImport();
75
- }, [startImport]);
76
- // Keyboard bindings
77
- useKeyBinding(KEYBINDINGS.UP, () => {
78
- if (step === 'source') {
79
- setSelectedSource(prev => Math.max(0, prev - 1));
80
- }
81
- });
82
- useKeyBinding(KEYBINDINGS.DOWN, () => {
83
- if (step === 'source') {
84
- setSelectedSource(prev => Math.min(SOURCES.length - 1, prev + 1));
85
- }
86
- });
87
- useKeyBinding(KEYBINDINGS.SELECT, () => {
88
- if (step === 'source')
89
- selectSource();
90
- else if (step === 'url')
91
- submitUrl();
92
- else if (step === 'name')
93
- submitName();
94
- else if (step === 'result')
95
- goBack();
96
- });
97
- useKeyBinding(KEYBINDINGS.BACK, goBack);
98
- // Escape key for skip name
99
- useEffect(() => {
100
- if (step === 'name') {
101
- const handleEscape = () => {
102
- startImport();
103
- };
104
- const stdin = process.stdin;
105
- stdin.on('keypress', handleEscape);
106
- return () => {
107
- stdin.off('keypress', handleEscape);
108
- };
109
- }
110
- return undefined;
111
- }, [step, startImport]);
112
- return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 1, children: [_jsx(Box, { borderStyle: "double", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.primary, children: "Import Playlist" }) }), error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.error, children: ["Error: ", error] }) })), step === 'source' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Select playlist source:" }), SOURCES.map((source, index) => (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { backgroundColor: index === selectedSource ? theme.colors.primary : undefined, color: index === selectedSource
113
- ? theme.colors.background
114
- : theme.colors.text, bold: index === selectedSource, children: [index === selectedSource ? '► ' : ' ', source.label] }) }, source.key)))] })), step === 'url' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { color: theme.colors.dim, children: ["Enter ", SOURCES[selectedSource].label, " playlist URL or ID:"] }), _jsx(Box, { paddingX: 1, children: _jsx(TextInput, { value: url, onChange: setUrl, onSubmit: submitUrl, placeholder: "Paste URL or ID here...", focus: true }) }), _jsxs(Text, { color: theme.colors.dim, children: ["Examples:", ' ', SOURCES[selectedSource].key === 'spotify'
115
- ? 'https://open.spotify.com/playlist/...'
116
- : 'https://www.youtube.com/playlist?list=...'] })] })), step === 'name' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: theme.colors.dim, children: "Custom playlist name (optional, Esc to skip):" }), _jsx(Box, { paddingX: 1, children: _jsx(TextInput, { value: customName, onChange: setCustomName, onSubmit: submitName, placeholder: "Leave empty to use original name", focus: true }) }), _jsx(Text, { color: theme.colors.dim, children: "Press Enter to import or Esc to skip" })] })), step === 'importing' && progress && (_jsx(ImportProgressComponent, { progress: progress })), step === 'result' && result && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.success, bold: true, children: "\u2713 Import completed!" }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: ["Playlist: ", result.playlistName] }) }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: ["Matched:", ' ', _jsx(Text, { color: theme.colors.primary, children: result.matched }), "/", result.total, " tracks"] }) }), result.errors.length > 0 && (_jsxs(Box, { flexDirection: "column", gap: 1, marginTop: 1, children: [_jsxs(Text, { color: theme.colors.dim, bold: true, children: ["Errors (", result.errors.length, "):"] }), result.errors.slice(0, 5).map((err, i) => (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: theme.colors.error, children: ["\u2022 ", err] }) }, i))), result.errors.length > 5 && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: theme.colors.dim, children: ["... and ", result.errors.length - 5, " more"] }) }))] })), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Press Enter to continue" }) })] })), step !== 'importing' && step !== 'result' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: step === 'source'
117
- ? '↑↓ to select, Enter to continue, Esc/q to go back'
118
- : 'Enter to continue, Esc to go back' }) }))] }));
119
- }
@@ -1,73 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // Import progress display component
3
- import { useEffect, useState } from 'react';
4
- import { Box, Text } from 'ink';
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- const PROGRESS_BLOCKS = 20;
7
- export default function ImportProgress({ progress }) {
8
- const { theme } = useTheme();
9
- const [animatedBlocks, setAnimatedBlocks] = useState(0);
10
- // Animate progress blocks
11
- useEffect(() => {
12
- if (progress.total > 0) {
13
- const targetBlocks = Math.floor((progress.current / progress.total) * PROGRESS_BLOCKS);
14
- const delay = Math.max(50, 500 / PROGRESS_BLOCKS); // Max 500ms total animation
15
- const interval = setInterval(() => {
16
- setAnimatedBlocks(prev => {
17
- if (prev >= targetBlocks) {
18
- clearInterval(interval);
19
- return prev;
20
- }
21
- return prev + 1;
22
- });
23
- }, delay);
24
- return () => clearInterval(interval);
25
- }
26
- return undefined;
27
- }, [progress]);
28
- // Calculate completed blocks
29
- const completedBlocks = Math.min(animatedBlocks, PROGRESS_BLOCKS);
30
- const progressPercent = progress.total > 0
31
- ? Math.round((progress.current / progress.total) * 100)
32
- : 0;
33
- // Get status color
34
- const getStatusColor = () => {
35
- switch (progress.status) {
36
- case 'fetching':
37
- return theme.colors.accent;
38
- case 'matching':
39
- return theme.colors.primary;
40
- case 'creating':
41
- return theme.colors.success;
42
- case 'completed':
43
- return theme.colors.success;
44
- case 'failed':
45
- case 'cancelled':
46
- return theme.colors.error;
47
- default:
48
- return theme.colors.dim;
49
- }
50
- };
51
- // Get status label
52
- const getStatusLabel = () => {
53
- switch (progress.status) {
54
- case 'fetching':
55
- return 'Fetching playlist...';
56
- case 'matching':
57
- return 'Matching tracks...';
58
- case 'creating':
59
- return 'Creating playlist...';
60
- case 'completed':
61
- return 'Completed!';
62
- case 'failed':
63
- return 'Failed';
64
- case 'cancelled':
65
- return 'Cancelled';
66
- default:
67
- return 'Starting...';
68
- }
69
- };
70
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { bold: true, color: getStatusColor(), children: getStatusLabel() }) }), progress.total > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Box, { children: [Array.from({ length: completedBlocks }).map((_, i) => (_jsx(Text, { backgroundColor: theme.colors.primary, children: ' ' }, i))), Array.from({ length: PROGRESS_BLOCKS - completedBlocks }).map((_, i) => (_jsx(Text, { backgroundColor: theme.colors.dim, dimColor: true, children: ' ' }, i + completedBlocks)))] }) })), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.colors.dim, children: [progress.total > 0 ? `${progressPercent}%` : '...', " -", ' ', progress.current, "/", progress.total || '?', ' ', progress.total === 1 ? 'track' : 'tracks'] }) }), progress.currentTrack && progress.status === 'matching' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.text, dimColor: true, children: progress.currentTrack.length > 50
71
- ? `...${progress.currentTrack.slice(-47)}`
72
- : progress.currentTrack }) })), progress.message && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.colors.dim, children: progress.message }) }))] }));
73
- }
@@ -1,72 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- // Explore / Genre browsing view — shows curated sections from YouTube Music
3
- import { Box, Text, useInput } from 'ink';
4
- import { useState, useEffect } from 'react';
5
- import { useTheme } from "../../hooks/useTheme.js";
6
- import { useNavigation } from "../../hooks/useNavigation.js";
7
- import { usePlayer } from "../../hooks/usePlayer.js";
8
- import { getMusicService } from "../../services/youtube-music/api.js";
9
- export default function ExploreLayout() {
10
- const { theme } = useTheme();
11
- const { dispatch } = useNavigation();
12
- const { play } = usePlayer();
13
- const [sections, setSections] = useState([]);
14
- const [sectionIndex, setSectionIndex] = useState(0);
15
- const [trackIndex, setTrackIndex] = useState(0);
16
- const [isLoading, setIsLoading] = useState(true);
17
- const [error, setError] = useState(null);
18
- useEffect(() => {
19
- let cancelled = false;
20
- getMusicService()
21
- .getExploreSections()
22
- .then(results => {
23
- if (!cancelled) {
24
- setSections(results);
25
- setIsLoading(false);
26
- }
27
- })
28
- .catch((err) => {
29
- if (!cancelled) {
30
- setError(err instanceof Error ? err.message : 'Failed to load explore');
31
- setIsLoading(false);
32
- }
33
- });
34
- return () => {
35
- cancelled = true;
36
- };
37
- }, []);
38
- const currentSection = sections[sectionIndex];
39
- const tracks = currentSection?.tracks ?? [];
40
- useInput((input, key) => {
41
- if (key.escape) {
42
- dispatch({ category: 'GO_BACK' });
43
- return;
44
- }
45
- if (key.leftArrow || input === 'h') {
46
- setSectionIndex(i => Math.max(0, i - 1));
47
- setTrackIndex(0);
48
- }
49
- else if (key.rightArrow || input === 'l') {
50
- setSectionIndex(i => Math.min(sections.length - 1, i + 1));
51
- setTrackIndex(0);
52
- }
53
- else if (key.upArrow || input === 'k') {
54
- setTrackIndex(i => Math.max(0, i - 1));
55
- }
56
- else if (key.downArrow || input === 'j') {
57
- setTrackIndex(i => Math.min(tracks.length - 1, i + 1));
58
- }
59
- else if (key.return) {
60
- const track = tracks[trackIndex];
61
- if (track)
62
- play(track);
63
- }
64
- });
65
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: "\uD83C\uDFB5 Explore" }) }), isLoading ? (_jsx(Text, { color: theme.colors.dim, children: "Loading explore sections..." })) : error ? (_jsx(Text, { color: theme.colors.error, children: error })) : sections.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No sections found" })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, gap: 2, children: sections.map((section, index) => (_jsx(Text, { color: index === sectionIndex
66
- ? theme.colors.primary
67
- : theme.colors.dim, bold: index === sectionIndex, underline: index === sectionIndex, children: section.title }, section.title))) }), tracks.map((track, index) => {
68
- const isSelected = index === trackIndex;
69
- const artist = track.artists?.[0]?.name ?? 'Unknown';
70
- return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.dim, children: isSelected ? '▶ ' : `${String(index + 1).padStart(2)}. ` }), _jsx(Text, { color: isSelected ? theme.colors.primary : theme.colors.text, bold: isSelected, children: track.title }), _jsxs(Text, { color: theme.colors.dim, children: [" \u2014 ", artist] })] }, track.videoId));
71
- })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "\u2190/\u2192 Sections | \u2191/\u2193 Tracks | Enter Play | Esc Back" }) })] }));
72
- }
@@ -1,37 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { useTheme } from "../../hooks/useTheme.js";
4
- import { useHistory } from "../../stores/history.store.js";
5
- import { useTerminalSize } from "../../hooks/useTerminalSize.js";
6
- import { truncate } from "../../utils/format.js";
7
- import { useKeyBinding } from "../../hooks/useKeyboard.js";
8
- import { KEYBINDINGS } from "../../utils/constants.js";
9
- import { useNavigation } from "../../hooks/useNavigation.js";
10
- const DATE_FORMATTER = new Intl.DateTimeFormat(undefined, {
11
- dateStyle: 'medium',
12
- timeStyle: 'short',
13
- });
14
- function formatTimestamp(iso) {
15
- const date = new Date(iso);
16
- if (Number.isNaN(date.getTime())) {
17
- return iso;
18
- }
19
- return DATE_FORMATTER.format(date);
20
- }
21
- export default function HistoryLayout() {
22
- const { theme } = useTheme();
23
- const { history } = useHistory();
24
- const { columns } = useTerminalSize();
25
- const { dispatch } = useNavigation();
26
- useKeyBinding(KEYBINDINGS.BACK, () => {
27
- dispatch({ category: 'GO_BACK' });
28
- });
29
- const maxTitleLength = Math.max(30, columns - 20);
30
- return (_jsxs(Box, { flexDirection: "column", padding: 1, gap: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: "Recently Played" }) }), history.length === 0 ? (_jsx(Text, { color: theme.colors.dim, children: "No listening history yet." })) : (history.map(entry => {
31
- const artists = entry.track.artists
32
- ?.map(artist => artist.name)
33
- .join(', ')
34
- .trim();
35
- return (_jsxs(Box, { flexDirection: "column", paddingY: 1, borderStyle: "round", borderColor: theme.colors.dim, children: [_jsx(Text, { color: theme.colors.secondary, children: formatTimestamp(entry.playedAt) }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.text, bold: true, children: truncate(entry.track.title, maxTitleLength) }), _jsx(Text, { color: theme.colors.dim, children: artists ? ` • ${artists}` : '' })] }), entry.track.album?.name && (_jsxs(Text, { color: theme.colors.dim, children: ["Album: ", entry.track.album.name] }))] }, `${entry.playedAt}-${entry.track.videoId}`));
36
- })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Esc to go back \u2022 Shift+H to reopen history" }) })] }));
37
- }