@involvex/youtube-music-cli 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/LICENSE +21 -0
- package/dist/source/cli.js +21 -0
- package/dist/source/components/export/ExportLayout.d.ts +1 -0
- package/dist/source/components/export/ExportLayout.js +111 -0
- package/dist/source/components/layouts/MainLayout.js +19 -0
- package/dist/source/components/settings/Settings.js +6 -2
- package/dist/source/main.js +20 -0
- package/dist/source/services/config/config.service.d.ts +15 -0
- package/dist/source/services/config/config.service.js +31 -0
- package/dist/source/services/export/export.service.d.ts +41 -0
- package/dist/source/services/export/export.service.js +131 -0
- package/dist/source/services/player/player.service.d.ts +12 -0
- package/dist/source/services/player/player.service.js +30 -0
- package/dist/source/services/version-check/version-check.service.d.ts +32 -0
- package/dist/source/services/version-check/version-check.service.js +121 -0
- package/dist/source/types/actions.d.ts +3 -0
- package/dist/source/types/config.types.d.ts +7 -0
- package/dist/source/types/navigation.types.d.ts +2 -2
- package/dist/source/utils/constants.d.ts +3 -1
- package/dist/source/utils/constants.js +3 -1
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 involvex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/source/cli.js
CHANGED
|
@@ -9,6 +9,9 @@ import { getPluginRegistryService } from "./services/plugin/plugin-registry.serv
|
|
|
9
9
|
import { getImportService } from "./services/import/import.service.js";
|
|
10
10
|
import { getWebServerManager } from "./services/web/web-server-manager.js";
|
|
11
11
|
import { getWebStreamingService } from "./services/web/web-streaming.service.js";
|
|
12
|
+
import { getVersionCheckService } from "./services/version-check/version-check.service.js";
|
|
13
|
+
import { getConfigService } from "./services/config/config.service.js";
|
|
14
|
+
import { APP_VERSION } from "./utils/constants.js";
|
|
12
15
|
const cli = meow(`
|
|
13
16
|
Usage
|
|
14
17
|
$ youtube-music-cli
|
|
@@ -350,6 +353,24 @@ else {
|
|
|
350
353
|
})();
|
|
351
354
|
}
|
|
352
355
|
else {
|
|
356
|
+
// Check for updates before rendering the app (skip in web-only mode)
|
|
357
|
+
if (!cli.flags.webOnly) {
|
|
358
|
+
void (async () => {
|
|
359
|
+
const versionCheck = getVersionCheckService();
|
|
360
|
+
const config = getConfigService();
|
|
361
|
+
const lastCheck = config.getLastVersionCheck();
|
|
362
|
+
if (versionCheck.shouldCheck(lastCheck)) {
|
|
363
|
+
const result = await versionCheck.checkForUpdates(APP_VERSION);
|
|
364
|
+
config.setLastVersionCheck(versionCheck.markChecked());
|
|
365
|
+
if (result.hasUpdate) {
|
|
366
|
+
console.log('');
|
|
367
|
+
console.log(` Update available: ${APP_VERSION} → ${result.latestVersion}`);
|
|
368
|
+
console.log('Run: npm install -g @involvex/youtube-music-cli');
|
|
369
|
+
console.log('');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
}
|
|
353
374
|
// Render the app
|
|
354
375
|
render(_jsx(App, { flags: cli.flags }));
|
|
355
376
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ExportLayout(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
}
|
|
@@ -21,9 +21,12 @@ import KeybindingsLayout from "../config/KeybindingsLayout.js";
|
|
|
21
21
|
import TrendingLayout from "./TrendingLayout.js";
|
|
22
22
|
import ExploreLayout from "./ExploreLayout.js";
|
|
23
23
|
import ImportLayout from "../import/ImportLayout.js";
|
|
24
|
+
import ExportLayout from "../export/ExportLayout.js";
|
|
24
25
|
import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
|
|
25
26
|
import { Box } from 'ink';
|
|
26
27
|
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
28
|
+
import { getPlayerService } from "../../services/player/player.service.js";
|
|
29
|
+
import { getConfigService } from "../../services/config/config.service.js";
|
|
27
30
|
function MainLayout() {
|
|
28
31
|
const { theme } = useTheme();
|
|
29
32
|
const { state: navState, dispatch } = useNavigation();
|
|
@@ -75,6 +78,19 @@ function MainLayout() {
|
|
|
75
78
|
dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
|
|
76
79
|
}
|
|
77
80
|
}, [dispatch, navState.currentView]);
|
|
81
|
+
const handleDetach = useCallback(() => {
|
|
82
|
+
// Detach mode: Exit CLI while keeping music playing
|
|
83
|
+
const player = getPlayerService();
|
|
84
|
+
const config = getConfigService();
|
|
85
|
+
// Get the IPC path and current URL before detaching
|
|
86
|
+
const { ipcPath, currentUrl } = player.detach();
|
|
87
|
+
// Save the background playback state if we have an active session
|
|
88
|
+
if (ipcPath && currentUrl) {
|
|
89
|
+
config.setBackgroundPlaybackState({ ipcPath, currentUrl });
|
|
90
|
+
}
|
|
91
|
+
// Exit the app
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}, []);
|
|
78
94
|
const togglePlayerMode = useCallback(() => {
|
|
79
95
|
dispatch({ category: 'TOGGLE_PLAYER_MODE' });
|
|
80
96
|
}, [dispatch]);
|
|
@@ -91,6 +107,7 @@ function MainLayout() {
|
|
|
91
107
|
useKeyBinding(['T'], goToTrending);
|
|
92
108
|
useKeyBinding(['e'], goToExplore);
|
|
93
109
|
useKeyBinding(['i'], goToImport);
|
|
110
|
+
useKeyBinding(KEYBINDINGS.DETACH, handleDetach);
|
|
94
111
|
// Memoize the view component to prevent unnecessary remounts
|
|
95
112
|
// Only recreate when currentView actually changes
|
|
96
113
|
const currentView = useMemo(() => {
|
|
@@ -127,6 +144,8 @@ function MainLayout() {
|
|
|
127
144
|
return _jsx(ExploreLayout, {}, "explore");
|
|
128
145
|
case 'import':
|
|
129
146
|
return _jsx(ImportLayout, {}, "import");
|
|
147
|
+
case 'export_playlists':
|
|
148
|
+
return _jsx(ExportLayout, {}, "export_playlists");
|
|
130
149
|
case 'help':
|
|
131
150
|
return _jsx(Help, {}, "help");
|
|
132
151
|
default:
|
|
@@ -23,6 +23,7 @@ const SETTINGS_ITEMS = [
|
|
|
23
23
|
'Download Format',
|
|
24
24
|
'Sleep Timer',
|
|
25
25
|
'Import Playlists',
|
|
26
|
+
'Export Playlists',
|
|
26
27
|
'Custom Keybindings',
|
|
27
28
|
'Manage Plugins',
|
|
28
29
|
];
|
|
@@ -120,9 +121,12 @@ export default function Settings() {
|
|
|
120
121
|
dispatch({ category: 'NAVIGATE', view: VIEW.IMPORT });
|
|
121
122
|
}
|
|
122
123
|
else if (selectedIndex === 9) {
|
|
123
|
-
dispatch({ category: 'NAVIGATE', view: VIEW.
|
|
124
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.EXPORT_PLAYLISTS });
|
|
124
125
|
}
|
|
125
126
|
else if (selectedIndex === 10) {
|
|
127
|
+
dispatch({ category: 'NAVIGATE', view: VIEW.KEYBINDINGS });
|
|
128
|
+
}
|
|
129
|
+
else if (selectedIndex === 11) {
|
|
126
130
|
dispatch({ category: 'NAVIGATE', view: VIEW.PLUGINS });
|
|
127
131
|
}
|
|
128
132
|
};
|
|
@@ -146,5 +150,5 @@ export default function Settings() {
|
|
|
146
150
|
? theme.colors.background
|
|
147
151
|
: isActive
|
|
148
152
|
? theme.colors.accent
|
|
149
|
-
: theme.colors.text, bold: selectedIndex === 7, children: sleepTimerLabel }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 8 ? theme.colors.primary : undefined, color: selectedIndex === 8 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 8, children: "Import Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 9 ? theme.colors.primary : undefined, color: selectedIndex === 9 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 9, children: "
|
|
153
|
+
: theme.colors.text, bold: selectedIndex === 7, children: sleepTimerLabel }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 8 ? theme.colors.primary : undefined, color: selectedIndex === 8 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 8, children: "Import Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 9 ? theme.colors.primary : undefined, color: selectedIndex === 9 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 9, children: "Export Playlists \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 10 ? theme.colors.primary : undefined, color: selectedIndex === 10 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 10, children: "Custom Keybindings \u2192" }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: selectedIndex === 11 ? theme.colors.primary : undefined, color: selectedIndex === 11 ? theme.colors.background : theme.colors.text, bold: selectedIndex === 11, children: "Manage Plugins" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.dim, children: "Arrows to navigate, Enter to select, Esc/q to go back" }) })] }));
|
|
150
154
|
}
|
package/dist/source/main.js
CHANGED
|
@@ -14,11 +14,31 @@ import { useNavigation } from "./hooks/useNavigation.js";
|
|
|
14
14
|
import { usePlayer } from "./hooks/usePlayer.js";
|
|
15
15
|
import { useYouTubeMusic } from "./hooks/useYouTubeMusic.js";
|
|
16
16
|
import { VIEW } from "./utils/constants.js";
|
|
17
|
+
import { getConfigService } from "./services/config/config.service.js";
|
|
18
|
+
import { getPlayerService } from "./services/player/player.service.js";
|
|
19
|
+
import { getNotificationService } from "./services/notification/notification.service.js";
|
|
17
20
|
function Initializer({ flags }) {
|
|
18
21
|
const { dispatch } = useNavigation();
|
|
19
22
|
const { play } = usePlayer();
|
|
20
23
|
const { getTrack, getPlaylist } = useYouTubeMusic();
|
|
21
24
|
useEffect(() => {
|
|
25
|
+
// Check for background playback state on startup
|
|
26
|
+
const config = getConfigService();
|
|
27
|
+
const player = getPlayerService();
|
|
28
|
+
const backgroundState = config.getBackgroundPlaybackState();
|
|
29
|
+
if (backgroundState.enabled) {
|
|
30
|
+
// Show notification about background playback
|
|
31
|
+
const notification = getNotificationService();
|
|
32
|
+
notification.setEnabled(true);
|
|
33
|
+
void notification.notify('Background Playback Active', 'Press R to resume control');
|
|
34
|
+
// Try to reattach to the existing mpv process
|
|
35
|
+
if (backgroundState.ipcPath) {
|
|
36
|
+
void player.reattach(backgroundState.ipcPath).catch(() => {
|
|
37
|
+
// Reattach failed, clear the state
|
|
38
|
+
config.clearBackgroundPlaybackState();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
22
42
|
if (flags?.showSuggestions) {
|
|
23
43
|
dispatch({ category: 'NAVIGATE', view: VIEW.SUGGESTIONS });
|
|
24
44
|
}
|
|
@@ -23,6 +23,21 @@ declare class ConfigService {
|
|
|
23
23
|
removeFavorite(trackId: string): void;
|
|
24
24
|
isFavorite(trackId: string): boolean;
|
|
25
25
|
getFavorites(): string[];
|
|
26
|
+
setBackgroundPlaybackState(state: {
|
|
27
|
+
ipcPath: string;
|
|
28
|
+
currentUrl: string;
|
|
29
|
+
}): void;
|
|
30
|
+
getBackgroundPlaybackState(): {
|
|
31
|
+
enabled: true;
|
|
32
|
+
ipcPath: string;
|
|
33
|
+
currentUrl: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
} | {
|
|
36
|
+
enabled: false;
|
|
37
|
+
};
|
|
38
|
+
clearBackgroundPlaybackState(): void;
|
|
39
|
+
setLastVersionCheck(timestamp: string): void;
|
|
40
|
+
getLastVersionCheck(): string | undefined;
|
|
26
41
|
}
|
|
27
42
|
export declare function getConfigService(): ConfigService;
|
|
28
43
|
export {};
|
|
@@ -145,6 +145,37 @@ class ConfigService {
|
|
|
145
145
|
getFavorites() {
|
|
146
146
|
return this.config.favorites;
|
|
147
147
|
}
|
|
148
|
+
setBackgroundPlaybackState(state) {
|
|
149
|
+
this.config.backgroundPlayback = {
|
|
150
|
+
enabled: true,
|
|
151
|
+
ipcPath: state.ipcPath,
|
|
152
|
+
currentUrl: state.currentUrl,
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
this.save();
|
|
156
|
+
}
|
|
157
|
+
getBackgroundPlaybackState() {
|
|
158
|
+
if (this.config.backgroundPlayback?.enabled) {
|
|
159
|
+
return {
|
|
160
|
+
enabled: true,
|
|
161
|
+
ipcPath: this.config.backgroundPlayback.ipcPath,
|
|
162
|
+
currentUrl: this.config.backgroundPlayback.currentUrl,
|
|
163
|
+
timestamp: this.config.backgroundPlayback.timestamp,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { enabled: false };
|
|
167
|
+
}
|
|
168
|
+
clearBackgroundPlaybackState() {
|
|
169
|
+
this.config.backgroundPlayback = undefined;
|
|
170
|
+
this.save();
|
|
171
|
+
}
|
|
172
|
+
setLastVersionCheck(timestamp) {
|
|
173
|
+
this.config.lastVersionCheck = timestamp;
|
|
174
|
+
this.save();
|
|
175
|
+
}
|
|
176
|
+
getLastVersionCheck() {
|
|
177
|
+
return this.config.lastVersionCheck;
|
|
178
|
+
}
|
|
148
179
|
}
|
|
149
180
|
// Singleton instance
|
|
150
181
|
let configServiceInstance = null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Playlist } from '../../types/youtube-music.types.ts';
|
|
2
|
+
export type ExportFormat = 'json' | 'm3u8' | 'both';
|
|
3
|
+
export interface ExportOptions {
|
|
4
|
+
format: ExportFormat;
|
|
5
|
+
outputDir?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ExportResult {
|
|
8
|
+
playlistName: string;
|
|
9
|
+
format: ExportFormat;
|
|
10
|
+
files: string[];
|
|
11
|
+
success: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
declare class ExportService {
|
|
15
|
+
private static instance;
|
|
16
|
+
private readonly DEFAULT_EXPORT_DIR;
|
|
17
|
+
private constructor();
|
|
18
|
+
static getInstance(): ExportService;
|
|
19
|
+
/**
|
|
20
|
+
* Get the export directory, create if it doesn't exist
|
|
21
|
+
*/
|
|
22
|
+
private getExportDir;
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize filename for safe file system usage
|
|
25
|
+
*/
|
|
26
|
+
private sanitizeFilename;
|
|
27
|
+
/**
|
|
28
|
+
* Generate M3U8 format content for a playlist
|
|
29
|
+
*/
|
|
30
|
+
private generateM3U8;
|
|
31
|
+
/**
|
|
32
|
+
* Export a single playlist to the specified format(s)
|
|
33
|
+
*/
|
|
34
|
+
exportPlaylist(playlist: Playlist, options: ExportOptions): Promise<ExportResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Export multiple playlists to the specified format(s)
|
|
37
|
+
*/
|
|
38
|
+
exportAllPlaylists(playlists: Playlist[], options: ExportOptions): Promise<ExportResult[]>;
|
|
39
|
+
}
|
|
40
|
+
export declare const getExportService: () => ExportService;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Playlist export service for JSON and M3U8 formats
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { CONFIG_DIR } from "../../utils/constants.js";
|
|
4
|
+
import { logger } from "../logger/logger.service.js";
|
|
5
|
+
class ExportService {
|
|
6
|
+
static instance;
|
|
7
|
+
DEFAULT_EXPORT_DIR = `${CONFIG_DIR}/exports`;
|
|
8
|
+
constructor() { }
|
|
9
|
+
static getInstance() {
|
|
10
|
+
if (!ExportService.instance) {
|
|
11
|
+
ExportService.instance = new ExportService();
|
|
12
|
+
}
|
|
13
|
+
return ExportService.instance;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the export directory, create if it doesn't exist
|
|
17
|
+
*/
|
|
18
|
+
getExportDir(customDir) {
|
|
19
|
+
const exportDir = customDir || this.DEFAULT_EXPORT_DIR;
|
|
20
|
+
if (!existsSync(exportDir)) {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(exportDir, { recursive: true });
|
|
23
|
+
logger.info('ExportService', 'Created export directory', { exportDir });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
logger.error('ExportService', 'Failed to create export directory', {
|
|
27
|
+
error: error instanceof Error ? error.message : String(error),
|
|
28
|
+
});
|
|
29
|
+
throw new Error(`Failed to create export directory: ${exportDir}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return exportDir;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sanitize filename for safe file system usage
|
|
36
|
+
*/
|
|
37
|
+
sanitizeFilename(name) {
|
|
38
|
+
// Remove or replace characters that are unsafe for filenames
|
|
39
|
+
return name
|
|
40
|
+
.replace(/[<>:"/\\|?*]/g, '') // Remove unsafe chars
|
|
41
|
+
.replace(/\s+/g, '_') // Replace spaces with underscores
|
|
42
|
+
.substring(0, 200); // Limit length
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Generate M3U8 format content for a playlist
|
|
46
|
+
*/
|
|
47
|
+
generateM3U8(playlist) {
|
|
48
|
+
const lines = ['#EXTM3U', ''];
|
|
49
|
+
for (const track of playlist.tracks) {
|
|
50
|
+
if (track.artists && track.artists.length > 0) {
|
|
51
|
+
const artistNames = track.artists.map(a => a.name).join(', ');
|
|
52
|
+
const duration = track.duration
|
|
53
|
+
? Math.round(track.duration / 1000)
|
|
54
|
+
: -1;
|
|
55
|
+
lines.push(`#EXTINF:${duration},${artistNames} - ${track.title}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
lines.push(`#EXTINF:-1,${track.title}`);
|
|
59
|
+
}
|
|
60
|
+
// Use the videoId to generate YouTube URL
|
|
61
|
+
if (track.videoId) {
|
|
62
|
+
lines.push(`https://www.youtube.com/watch?v=${track.videoId}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Export a single playlist to the specified format(s)
|
|
69
|
+
*/
|
|
70
|
+
async exportPlaylist(playlist, options) {
|
|
71
|
+
try {
|
|
72
|
+
logger.info('ExportService', 'Exporting playlist', {
|
|
73
|
+
playlist: playlist.name,
|
|
74
|
+
format: options.format,
|
|
75
|
+
});
|
|
76
|
+
const exportDir = this.getExportDir(options.outputDir);
|
|
77
|
+
const sanitizedName = this.sanitizeFilename(playlist.name);
|
|
78
|
+
const files = [];
|
|
79
|
+
// Export to JSON
|
|
80
|
+
if (options.format === 'json' || options.format === 'both') {
|
|
81
|
+
const jsonPath = `${exportDir}/${sanitizedName}.json`;
|
|
82
|
+
const jsonContent = JSON.stringify(playlist, null, 2);
|
|
83
|
+
writeFileSync(jsonPath, jsonContent, 'utf-8');
|
|
84
|
+
files.push(jsonPath);
|
|
85
|
+
logger.info('ExportService', 'Exported to JSON', { path: jsonPath });
|
|
86
|
+
}
|
|
87
|
+
// Export to M3U8
|
|
88
|
+
if (options.format === 'm3u8' || options.format === 'both') {
|
|
89
|
+
const m3u8Path = `${exportDir}/${sanitizedName}.m3u8`;
|
|
90
|
+
const m3u8Content = this.generateM3U8(playlist);
|
|
91
|
+
writeFileSync(m3u8Path, m3u8Content, 'utf-8');
|
|
92
|
+
files.push(m3u8Path);
|
|
93
|
+
logger.info('ExportService', 'Exported to M3U8', { path: m3u8Path });
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
playlistName: playlist.name,
|
|
97
|
+
format: options.format,
|
|
98
|
+
files,
|
|
99
|
+
success: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logger.error('ExportService', 'Failed to export playlist', {
|
|
104
|
+
error: error instanceof Error ? error.message : String(error),
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
playlistName: playlist.name,
|
|
108
|
+
format: options.format,
|
|
109
|
+
files: [],
|
|
110
|
+
success: false,
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Export multiple playlists to the specified format(s)
|
|
117
|
+
*/
|
|
118
|
+
async exportAllPlaylists(playlists, options) {
|
|
119
|
+
logger.info('ExportService', 'Exporting all playlists', {
|
|
120
|
+
count: playlists.length,
|
|
121
|
+
format: options.format,
|
|
122
|
+
});
|
|
123
|
+
const results = [];
|
|
124
|
+
for (const playlist of playlists) {
|
|
125
|
+
const result = await this.exportPlaylist(playlist, options);
|
|
126
|
+
results.push(result);
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export const getExportService = () => ExportService.getInstance();
|
|
@@ -54,6 +54,18 @@ declare class PlayerService {
|
|
|
54
54
|
pause(): void;
|
|
55
55
|
resume(): void;
|
|
56
56
|
stop(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Detach mode: Save state and clear references without killing mpv process
|
|
59
|
+
* Returns the IPC path and current URL for later reattachment
|
|
60
|
+
*/
|
|
61
|
+
detach(): {
|
|
62
|
+
ipcPath: string | null;
|
|
63
|
+
currentUrl: string | null;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Reattach to an existing mpv process via IPC
|
|
67
|
+
*/
|
|
68
|
+
reattach(ipcPath: string): Promise<void>;
|
|
57
69
|
setVolume(volume: number): void;
|
|
58
70
|
getVolume(): number;
|
|
59
71
|
setSpeed(speed: number): void;
|
|
@@ -360,6 +360,36 @@ class PlayerService {
|
|
|
360
360
|
this.ipcPath = null;
|
|
361
361
|
this.ipcConnectRetries = 0;
|
|
362
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Detach mode: Save state and clear references without killing mpv process
|
|
365
|
+
* Returns the IPC path and current URL for later reattachment
|
|
366
|
+
*/
|
|
367
|
+
detach() {
|
|
368
|
+
logger.info('PlayerService', 'Detaching from player', {
|
|
369
|
+
ipcPath: this.ipcPath,
|
|
370
|
+
currentUrl: this.currentUrl,
|
|
371
|
+
});
|
|
372
|
+
const info = {
|
|
373
|
+
ipcPath: this.ipcPath,
|
|
374
|
+
currentUrl: this.currentUrl,
|
|
375
|
+
};
|
|
376
|
+
// Clear references but DON'T kill mpv process - it keeps playing
|
|
377
|
+
this.mpvProcess = null;
|
|
378
|
+
this.ipcSocket = null;
|
|
379
|
+
this.ipcPath = null;
|
|
380
|
+
this.isPlaying = false;
|
|
381
|
+
return info;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Reattach to an existing mpv process via IPC
|
|
385
|
+
*/
|
|
386
|
+
async reattach(ipcPath) {
|
|
387
|
+
logger.info('PlayerService', 'Reattaching to player', { ipcPath });
|
|
388
|
+
this.ipcPath = ipcPath;
|
|
389
|
+
await this.connectIpc();
|
|
390
|
+
this.isPlaying = true;
|
|
391
|
+
logger.info('PlayerService', 'Successfully reattached to player');
|
|
392
|
+
}
|
|
363
393
|
setVolume(volume) {
|
|
364
394
|
logger.debug('PlayerService', 'setVolume() called', {
|
|
365
395
|
oldVolume: this.currentVolume,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface VersionCheckResult {
|
|
2
|
+
hasUpdate: boolean;
|
|
3
|
+
currentVersion: string;
|
|
4
|
+
latestVersion: string;
|
|
5
|
+
}
|
|
6
|
+
declare class VersionCheckService {
|
|
7
|
+
private static instance;
|
|
8
|
+
private readonly NPM_REGISTRY_URL;
|
|
9
|
+
private readonly CHECK_INTERVAL;
|
|
10
|
+
private constructor();
|
|
11
|
+
static getInstance(): VersionCheckService;
|
|
12
|
+
/**
|
|
13
|
+
* Compare two semantic version strings
|
|
14
|
+
* Returns: 1 if a > b, -1 if a < b, 0 if equal
|
|
15
|
+
*/
|
|
16
|
+
private compareVersions;
|
|
17
|
+
/**
|
|
18
|
+
* Check if a version check should be performed (once per 24 hours)
|
|
19
|
+
*/
|
|
20
|
+
shouldCheck(lastCheck: string | undefined): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Mark that a version check has been performed
|
|
23
|
+
* Returns the timestamp string to store
|
|
24
|
+
*/
|
|
25
|
+
markChecked(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Check npm registry for available updates
|
|
28
|
+
*/
|
|
29
|
+
checkForUpdates(currentVersion?: string): Promise<VersionCheckResult>;
|
|
30
|
+
}
|
|
31
|
+
export declare const getVersionCheckService: () => VersionCheckService;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Version check service for npm registry updates
|
|
2
|
+
import { APP_NAME, APP_VERSION } from "../../utils/constants.js";
|
|
3
|
+
import { logger } from "../logger/logger.service.js";
|
|
4
|
+
class VersionCheckService {
|
|
5
|
+
static instance;
|
|
6
|
+
NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
7
|
+
CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
8
|
+
constructor() { }
|
|
9
|
+
static getInstance() {
|
|
10
|
+
if (!VersionCheckService.instance) {
|
|
11
|
+
VersionCheckService.instance = new VersionCheckService();
|
|
12
|
+
}
|
|
13
|
+
return VersionCheckService.instance;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compare two semantic version strings
|
|
17
|
+
* Returns: 1 if a > b, -1 if a < b, 0 if equal
|
|
18
|
+
*/
|
|
19
|
+
compareVersions(a, b) {
|
|
20
|
+
const parseVersion = (v) => {
|
|
21
|
+
// Remove 'v' prefix if present and split by non-numeric chars
|
|
22
|
+
const clean = v.replace(/^v/i, '');
|
|
23
|
+
return clean.split(/[.-]/).map(part => {
|
|
24
|
+
const num = parseInt(part, 10);
|
|
25
|
+
return Number.isNaN(num) ? 0 : num;
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const partsA = parseVersion(a);
|
|
29
|
+
const partsB = parseVersion(b);
|
|
30
|
+
const maxLength = Math.max(partsA.length, partsB.length);
|
|
31
|
+
for (let i = 0; i < maxLength; i++) {
|
|
32
|
+
const partA = partsA[i] ?? 0;
|
|
33
|
+
const partB = partsB[i] ?? 0;
|
|
34
|
+
if (partA > partB)
|
|
35
|
+
return 1;
|
|
36
|
+
if (partA < partB)
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if a version check should be performed (once per 24 hours)
|
|
43
|
+
*/
|
|
44
|
+
shouldCheck(lastCheck) {
|
|
45
|
+
if (!lastCheck)
|
|
46
|
+
return true;
|
|
47
|
+
try {
|
|
48
|
+
const lastCheckDate = new Date(lastCheck);
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const diff = now.getTime() - lastCheckDate.getTime();
|
|
51
|
+
return diff >= this.CHECK_INTERVAL;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Mark that a version check has been performed
|
|
59
|
+
* Returns the timestamp string to store
|
|
60
|
+
*/
|
|
61
|
+
markChecked() {
|
|
62
|
+
return new Date().toISOString();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check npm registry for available updates
|
|
66
|
+
*/
|
|
67
|
+
async checkForUpdates(currentVersion = APP_VERSION) {
|
|
68
|
+
try {
|
|
69
|
+
logger.debug('VersionCheckService', 'Checking for updates', {
|
|
70
|
+
package: APP_NAME,
|
|
71
|
+
currentVersion,
|
|
72
|
+
});
|
|
73
|
+
const url = `${this.NPM_REGISTRY_URL}/${APP_NAME}`;
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
logger.warn('VersionCheckService', 'Failed to fetch package info', {
|
|
79
|
+
status: response.status,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
hasUpdate: false,
|
|
83
|
+
currentVersion,
|
|
84
|
+
latestVersion: currentVersion,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const data = (await response.json());
|
|
88
|
+
const latestVersion = data['dist-tags']?.latest;
|
|
89
|
+
if (!latestVersion) {
|
|
90
|
+
logger.warn('VersionCheckService', 'No latest version found in response');
|
|
91
|
+
return {
|
|
92
|
+
hasUpdate: false,
|
|
93
|
+
currentVersion,
|
|
94
|
+
latestVersion: currentVersion,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const hasUpdate = this.compareVersions(latestVersion, currentVersion) > 0;
|
|
98
|
+
logger.info('VersionCheckService', 'Version check complete', {
|
|
99
|
+
currentVersion,
|
|
100
|
+
latestVersion,
|
|
101
|
+
hasUpdate,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
hasUpdate,
|
|
105
|
+
currentVersion,
|
|
106
|
+
latestVersion,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
logger.error('VersionCheckService', 'Error checking for updates', {
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
hasUpdate: false,
|
|
115
|
+
currentVersion,
|
|
116
|
+
latestVersion: currentVersion,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export const getVersionCheckService = () => VersionCheckService.getInstance();
|
|
@@ -36,4 +36,11 @@ export interface Config {
|
|
|
36
36
|
downloadDirectory?: string;
|
|
37
37
|
downloadFormat?: DownloadFormat;
|
|
38
38
|
webServer?: WebServerConfig;
|
|
39
|
+
backgroundPlayback?: {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
ipcPath?: string;
|
|
42
|
+
currentUrl?: string;
|
|
43
|
+
timestamp?: string;
|
|
44
|
+
};
|
|
45
|
+
lastVersionCheck?: string;
|
|
39
46
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NavigateAction, GoBackAction, SetSearchQueryAction, SetSearchCategoryAction, SetSelectedResultAction, SetSelectedPlaylistAction, SetHasSearchedAction, SetSearchLimitAction, TogglePlayerModeAction } from './actions.ts';
|
|
1
|
+
import type { NavigateAction, GoBackAction, SetSearchQueryAction, SetSearchCategoryAction, SetSelectedResultAction, SetSelectedPlaylistAction, SetHasSearchedAction, SetSearchLimitAction, TogglePlayerModeAction, DetachAction } from './actions.ts';
|
|
2
2
|
export interface NavigationState {
|
|
3
3
|
currentView: string;
|
|
4
4
|
previousView: string | null;
|
|
@@ -12,4 +12,4 @@ export interface NavigationState {
|
|
|
12
12
|
history: string[];
|
|
13
13
|
playerMode: 'full' | 'mini';
|
|
14
14
|
}
|
|
15
|
-
export type NavigationAction = NavigateAction | GoBackAction | SetSearchQueryAction | SetSearchCategoryAction | SetSelectedResultAction | SetSelectedPlaylistAction | SetHasSearchedAction | SetSearchLimitAction | TogglePlayerModeAction;
|
|
15
|
+
export type NavigationAction = NavigateAction | GoBackAction | SetSearchQueryAction | SetSearchCategoryAction | SetSelectedResultAction | SetSelectedPlaylistAction | SetHasSearchedAction | SetSearchLimitAction | TogglePlayerModeAction | DetachAction;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const APP_NAME = "@involvex/youtube-music-cli";
|
|
2
|
-
export declare const APP_VERSION = "0.0.
|
|
2
|
+
export declare const APP_VERSION = "0.0.20";
|
|
3
3
|
export declare const CONFIG_DIR: string;
|
|
4
4
|
export declare const CONFIG_FILE: string;
|
|
5
5
|
export declare const VIEW: {
|
|
@@ -19,6 +19,7 @@ export declare const VIEW: {
|
|
|
19
19
|
readonly TRENDING: "trending";
|
|
20
20
|
readonly EXPLORE: "explore";
|
|
21
21
|
readonly IMPORT: "import";
|
|
22
|
+
readonly EXPORT_PLAYLISTS: "export_playlists";
|
|
22
23
|
};
|
|
23
24
|
export declare const SEARCH_TYPE: {
|
|
24
25
|
readonly ALL: "all";
|
|
@@ -35,6 +36,7 @@ export declare const KEYBINDINGS: {
|
|
|
35
36
|
readonly SUGGESTIONS: readonly ["g"];
|
|
36
37
|
readonly SETTINGS: readonly [","];
|
|
37
38
|
readonly PLUGINS: readonly ["p"];
|
|
39
|
+
readonly DETACH: readonly ["shift+q"];
|
|
38
40
|
readonly PLAY_PAUSE: readonly [" "];
|
|
39
41
|
readonly NEXT: readonly ["n", "right"];
|
|
40
42
|
readonly PREVIOUS: readonly ["b", "left"];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Application constants
|
|
2
2
|
export const APP_NAME = '@involvex/youtube-music-cli';
|
|
3
|
-
export const APP_VERSION = '0.0.
|
|
3
|
+
export const APP_VERSION = '0.0.20';
|
|
4
4
|
// Config directory
|
|
5
5
|
export const CONFIG_DIR = process.platform === 'win32'
|
|
6
6
|
? `${process.env['USERPROFILE']}\\.youtube-music-cli`
|
|
@@ -24,6 +24,7 @@ export const VIEW = {
|
|
|
24
24
|
TRENDING: 'trending',
|
|
25
25
|
EXPLORE: 'explore',
|
|
26
26
|
IMPORT: 'import',
|
|
27
|
+
EXPORT_PLAYLISTS: 'export_playlists',
|
|
27
28
|
};
|
|
28
29
|
// Search types
|
|
29
30
|
export const SEARCH_TYPE = {
|
|
@@ -43,6 +44,7 @@ export const KEYBINDINGS = {
|
|
|
43
44
|
SUGGESTIONS: ['g'],
|
|
44
45
|
SETTINGS: [','],
|
|
45
46
|
PLUGINS: ['p'],
|
|
47
|
+
DETACH: ['shift+q'],
|
|
46
48
|
// Player
|
|
47
49
|
PLAY_PAUSE: [' '],
|
|
48
50
|
NEXT: ['n', 'right'],
|
|
Binary file
|