@involvex/youtube-music-cli 0.0.46 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/cli.js.map +1004 -0
  3. package/dist/source/hooks/usePlayer.d.ts +1 -0
  4. package/dist/source/services/player-state/player-state.service.d.ts +1 -0
  5. package/dist/source/stores/player.store.d.ts +1 -0
  6. package/dist/source/types/actions.d.ts +4 -0
  7. package/dist/source/types/player.types.d.ts +3 -2
  8. package/dist/source/utils/constants.d.ts +1 -0
  9. package/dist/source/utils/icons.d.ts +1 -0
  10. package/dist/youtube-music-cli +0 -0
  11. package/package.json +1 -1
  12. package/dist/eslint.config.js +0 -55
  13. package/dist/package.json +0 -120
  14. package/dist/scripts/build-cli.js +0 -46
  15. package/dist/source/app.js +0 -17
  16. package/dist/source/cli.js +0 -504
  17. package/dist/source/components/common/ErrorBoundary.js +0 -22
  18. package/dist/source/components/common/Help.js +0 -18
  19. package/dist/source/components/common/ShortcutsBar.js +0 -80
  20. package/dist/source/components/config/ConfigLayout.js +0 -84
  21. package/dist/source/components/config/KeybindingsLayout.js +0 -107
  22. package/dist/source/components/export/ExportLayout.js +0 -111
  23. package/dist/source/components/import/ImportLayout.js +0 -119
  24. package/dist/source/components/import/ImportProgress.js +0 -73
  25. package/dist/source/components/layouts/ExploreLayout.js +0 -72
  26. package/dist/source/components/layouts/HistoryLayout.js +0 -37
  27. package/dist/source/components/layouts/LyricsLayout.js +0 -89
  28. package/dist/source/components/layouts/MainLayout.js +0 -190
  29. package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
  30. package/dist/source/components/layouts/PlayerLayout.js +0 -9
  31. package/dist/source/components/layouts/PluginsLayout.js +0 -77
  32. package/dist/source/components/layouts/SearchLayout.js +0 -193
  33. package/dist/source/components/layouts/TrendingLayout.js +0 -59
  34. package/dist/source/components/player/NowPlaying.js +0 -45
  35. package/dist/source/components/player/PlayerControls.js +0 -83
  36. package/dist/source/components/player/ProgressBar.js +0 -19
  37. package/dist/source/components/player/QueueList.js +0 -36
  38. package/dist/source/components/player/Suggestions.js +0 -50
  39. package/dist/source/components/playlist/PlaylistList.js +0 -138
  40. package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
  41. package/dist/source/components/plugins/PluginsAvailable.js +0 -55
  42. package/dist/source/components/plugins/PluginsList.js +0 -18
  43. package/dist/source/components/search/SearchBar.js +0 -55
  44. package/dist/source/components/search/SearchHistory.js +0 -35
  45. package/dist/source/components/search/SearchResults.js +0 -280
  46. package/dist/source/components/settings/Settings.js +0 -211
  47. package/dist/source/components/theme/ThemeSwitcher.js +0 -11
  48. package/dist/source/config/themes.config.js +0 -123
  49. package/dist/source/contexts/theme.context.js +0 -29
  50. package/dist/source/hooks/useKeyboard.js +0 -188
  51. package/dist/source/hooks/useKeyboardBlocker.js +0 -45
  52. package/dist/source/hooks/useNavigation.js +0 -5
  53. package/dist/source/hooks/usePlayer.js +0 -43
  54. package/dist/source/hooks/usePlaylist.js +0 -65
  55. package/dist/source/hooks/useSearch.js +0 -76
  56. package/dist/source/hooks/useSleepTimer.js +0 -48
  57. package/dist/source/hooks/useTerminalSize.js +0 -24
  58. package/dist/source/hooks/useTheme.js +0 -5
  59. package/dist/source/hooks/useYouTubeMusic.js +0 -112
  60. package/dist/source/main.js +0 -127
  61. package/dist/source/services/cache/cache.service.js +0 -67
  62. package/dist/source/services/completions/completions.service.js +0 -313
  63. package/dist/source/services/config/config.service.js +0 -191
  64. package/dist/source/services/discord/discord-rpc.service.js +0 -95
  65. package/dist/source/services/download/download.service.js +0 -350
  66. package/dist/source/services/export/export.service.js +0 -131
  67. package/dist/source/services/history/history.service.js +0 -83
  68. package/dist/source/services/import/import.service.js +0 -272
  69. package/dist/source/services/import/spotify.service.js +0 -171
  70. package/dist/source/services/import/track-matcher.service.js +0 -271
  71. package/dist/source/services/import/youtube-import.service.js +0 -84
  72. package/dist/source/services/logger/logger.service.js +0 -52
  73. package/dist/source/services/lyrics/lyrics.service.js +0 -93
  74. package/dist/source/services/mpris/mpris.service.js +0 -78
  75. package/dist/source/services/notification/notification.service.js +0 -57
  76. package/dist/source/services/player/dependency-check.service.js +0 -140
  77. package/dist/source/services/player/player.service.js +0 -478
  78. package/dist/source/services/player-state/player-state.service.js +0 -122
  79. package/dist/source/services/plugin/plugin-audio-api.js +0 -36
  80. package/dist/source/services/plugin/plugin-context.js +0 -256
  81. package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
  82. package/dist/source/services/plugin/plugin-installer.service.js +0 -248
  83. package/dist/source/services/plugin/plugin-loader.service.js +0 -161
  84. package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
  85. package/dist/source/services/plugin/plugin-registry.service.js +0 -215
  86. package/dist/source/services/plugin/plugin-ui-api.js +0 -46
  87. package/dist/source/services/plugin/plugin-updater.service.js +0 -206
  88. package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
  89. package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
  90. package/dist/source/services/version-check/version-check.service.js +0 -121
  91. package/dist/source/services/web/static-file.service.js +0 -185
  92. package/dist/source/services/web/web-server-manager.js +0 -506
  93. package/dist/source/services/web/web-streaming.service.js +0 -290
  94. package/dist/source/services/web/websocket.server.js +0 -267
  95. package/dist/source/services/youtube-music/api.js +0 -649
  96. package/dist/source/services/youtube-music/search.service.js +0 -38
  97. package/dist/source/stores/history.store.js +0 -64
  98. package/dist/source/stores/navigation.store.js +0 -90
  99. package/dist/source/stores/player.store.js +0 -724
  100. package/dist/source/stores/plugins.store.js +0 -177
  101. package/dist/source/types/actions.js +0 -1
  102. package/dist/source/types/cli.types.js +0 -1
  103. package/dist/source/types/config.types.js +0 -1
  104. package/dist/source/types/history.types.js +0 -1
  105. package/dist/source/types/import.types.js +0 -2
  106. package/dist/source/types/keyboard.types.js +0 -1
  107. package/dist/source/types/navigation.types.js +0 -1
  108. package/dist/source/types/player.types.js +0 -1
  109. package/dist/source/types/playlist.types.js +0 -1
  110. package/dist/source/types/plugin.types.js +0 -1
  111. package/dist/source/types/theme.types.js +0 -1
  112. package/dist/source/types/web.types.js +0 -2
  113. package/dist/source/types/youtube-music.types.js +0 -1
  114. package/dist/source/types/youtubei.types.js +0 -3
  115. package/dist/source/utils/constants.js +0 -134
  116. package/dist/source/utils/format.js +0 -24
  117. package/dist/source/utils/icons.js +0 -26
  118. 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
- }