@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.
- package/CHANGELOG.md +4 -0
- package/dist/cli.js.map +1004 -0
- package/dist/source/hooks/usePlayer.d.ts +1 -0
- package/dist/source/services/player-state/player-state.service.d.ts +1 -0
- package/dist/source/stores/player.store.d.ts +1 -0
- package/dist/source/types/actions.d.ts +4 -0
- package/dist/source/types/player.types.d.ts +3 -2
- package/dist/source/utils/constants.d.ts +1 -0
- package/dist/source/utils/icons.d.ts +1 -0
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
- package/dist/eslint.config.js +0 -55
- package/dist/package.json +0 -120
- package/dist/scripts/build-cli.js +0 -46
- package/dist/source/app.js +0 -17
- package/dist/source/cli.js +0 -504
- package/dist/source/components/common/ErrorBoundary.js +0 -22
- package/dist/source/components/common/Help.js +0 -18
- package/dist/source/components/common/ShortcutsBar.js +0 -80
- package/dist/source/components/config/ConfigLayout.js +0 -84
- package/dist/source/components/config/KeybindingsLayout.js +0 -107
- package/dist/source/components/export/ExportLayout.js +0 -111
- package/dist/source/components/import/ImportLayout.js +0 -119
- package/dist/source/components/import/ImportProgress.js +0 -73
- package/dist/source/components/layouts/ExploreLayout.js +0 -72
- package/dist/source/components/layouts/HistoryLayout.js +0 -37
- package/dist/source/components/layouts/LyricsLayout.js +0 -89
- package/dist/source/components/layouts/MainLayout.js +0 -190
- package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
- package/dist/source/components/layouts/PlayerLayout.js +0 -9
- package/dist/source/components/layouts/PluginsLayout.js +0 -77
- package/dist/source/components/layouts/SearchLayout.js +0 -193
- package/dist/source/components/layouts/TrendingLayout.js +0 -59
- package/dist/source/components/player/NowPlaying.js +0 -45
- package/dist/source/components/player/PlayerControls.js +0 -83
- package/dist/source/components/player/ProgressBar.js +0 -19
- package/dist/source/components/player/QueueList.js +0 -36
- package/dist/source/components/player/Suggestions.js +0 -50
- package/dist/source/components/playlist/PlaylistList.js +0 -138
- package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
- package/dist/source/components/plugins/PluginsAvailable.js +0 -55
- package/dist/source/components/plugins/PluginsList.js +0 -18
- package/dist/source/components/search/SearchBar.js +0 -55
- package/dist/source/components/search/SearchHistory.js +0 -35
- package/dist/source/components/search/SearchResults.js +0 -280
- package/dist/source/components/settings/Settings.js +0 -211
- package/dist/source/components/theme/ThemeSwitcher.js +0 -11
- package/dist/source/config/themes.config.js +0 -123
- package/dist/source/contexts/theme.context.js +0 -29
- package/dist/source/hooks/useKeyboard.js +0 -188
- package/dist/source/hooks/useKeyboardBlocker.js +0 -45
- package/dist/source/hooks/useNavigation.js +0 -5
- package/dist/source/hooks/usePlayer.js +0 -43
- package/dist/source/hooks/usePlaylist.js +0 -65
- package/dist/source/hooks/useSearch.js +0 -76
- package/dist/source/hooks/useSleepTimer.js +0 -48
- package/dist/source/hooks/useTerminalSize.js +0 -24
- package/dist/source/hooks/useTheme.js +0 -5
- package/dist/source/hooks/useYouTubeMusic.js +0 -112
- package/dist/source/main.js +0 -127
- package/dist/source/services/cache/cache.service.js +0 -67
- package/dist/source/services/completions/completions.service.js +0 -313
- package/dist/source/services/config/config.service.js +0 -191
- package/dist/source/services/discord/discord-rpc.service.js +0 -95
- package/dist/source/services/download/download.service.js +0 -350
- package/dist/source/services/export/export.service.js +0 -131
- package/dist/source/services/history/history.service.js +0 -83
- package/dist/source/services/import/import.service.js +0 -272
- package/dist/source/services/import/spotify.service.js +0 -171
- package/dist/source/services/import/track-matcher.service.js +0 -271
- package/dist/source/services/import/youtube-import.service.js +0 -84
- package/dist/source/services/logger/logger.service.js +0 -52
- package/dist/source/services/lyrics/lyrics.service.js +0 -93
- package/dist/source/services/mpris/mpris.service.js +0 -78
- package/dist/source/services/notification/notification.service.js +0 -57
- package/dist/source/services/player/dependency-check.service.js +0 -140
- package/dist/source/services/player/player.service.js +0 -478
- package/dist/source/services/player-state/player-state.service.js +0 -122
- package/dist/source/services/plugin/plugin-audio-api.js +0 -36
- package/dist/source/services/plugin/plugin-context.js +0 -256
- package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
- package/dist/source/services/plugin/plugin-installer.service.js +0 -248
- package/dist/source/services/plugin/plugin-loader.service.js +0 -161
- package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
- package/dist/source/services/plugin/plugin-registry.service.js +0 -215
- package/dist/source/services/plugin/plugin-ui-api.js +0 -46
- package/dist/source/services/plugin/plugin-updater.service.js +0 -206
- package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
- package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
- package/dist/source/services/version-check/version-check.service.js +0 -121
- package/dist/source/services/web/static-file.service.js +0 -185
- package/dist/source/services/web/web-server-manager.js +0 -506
- package/dist/source/services/web/web-streaming.service.js +0 -290
- package/dist/source/services/web/websocket.server.js +0 -267
- package/dist/source/services/youtube-music/api.js +0 -649
- package/dist/source/services/youtube-music/search.service.js +0 -38
- package/dist/source/stores/history.store.js +0 -64
- package/dist/source/stores/navigation.store.js +0 -90
- package/dist/source/stores/player.store.js +0 -724
- package/dist/source/stores/plugins.store.js +0 -177
- package/dist/source/types/actions.js +0 -1
- package/dist/source/types/cli.types.js +0 -1
- package/dist/source/types/config.types.js +0 -1
- package/dist/source/types/history.types.js +0 -1
- package/dist/source/types/import.types.js +0 -2
- package/dist/source/types/keyboard.types.js +0 -1
- package/dist/source/types/navigation.types.js +0 -1
- package/dist/source/types/player.types.js +0 -1
- package/dist/source/types/playlist.types.js +0 -1
- package/dist/source/types/plugin.types.js +0 -1
- package/dist/source/types/theme.types.js +0 -1
- package/dist/source/types/web.types.js +0 -2
- package/dist/source/types/youtube-music.types.js +0 -1
- package/dist/source/types/youtubei.types.js +0 -3
- package/dist/source/utils/constants.js +0 -134
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -26
- package/dist/source/utils/search-filters.js +0 -100
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { getPluginLoaderService } from "./plugin-loader.service.js";
|
|
2
|
-
import { getPluginPermissionsService } from "./plugin-permissions.service.js";
|
|
3
|
-
import { getConfigService } from "../config/config.service.js";
|
|
4
|
-
import { logger } from "../logger/logger.service.js";
|
|
5
|
-
import { CONFIG_DIR } from "../../utils/constants.js";
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
8
|
-
const PLUGINS_DIR = join(CONFIG_DIR, 'plugins');
|
|
9
|
-
/**
|
|
10
|
-
* Plugin registry service - manages all loaded plugins
|
|
11
|
-
*/
|
|
12
|
-
class PluginRegistryService {
|
|
13
|
-
plugins;
|
|
14
|
-
pluginLoader = getPluginLoaderService();
|
|
15
|
-
permissionsService = getPluginPermissionsService();
|
|
16
|
-
configService = getConfigService();
|
|
17
|
-
constructor() {
|
|
18
|
-
this.plugins = new Map();
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Load a plugin from a directory
|
|
22
|
-
*/
|
|
23
|
-
async loadPlugin(pluginPath) {
|
|
24
|
-
const instance = await this.pluginLoader.loadPlugin(pluginPath);
|
|
25
|
-
// Check if already loaded
|
|
26
|
-
if (this.plugins.has(instance.manifest.id)) {
|
|
27
|
-
throw new Error(`Plugin ${instance.manifest.id} is already loaded`);
|
|
28
|
-
}
|
|
29
|
-
// Store in registry
|
|
30
|
-
this.plugins.set(instance.manifest.id, instance);
|
|
31
|
-
logger.info('PluginRegistryService', `Registered plugin: ${instance.manifest.name}`);
|
|
32
|
-
return instance;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Unload a plugin
|
|
36
|
-
*/
|
|
37
|
-
async unloadPlugin(pluginId) {
|
|
38
|
-
const instance = this.plugins.get(pluginId);
|
|
39
|
-
if (!instance) {
|
|
40
|
-
throw new Error(`Plugin ${pluginId} is not loaded`);
|
|
41
|
-
}
|
|
42
|
-
// Call destroy hook if enabled
|
|
43
|
-
if (instance.enabled && instance.plugin.destroy) {
|
|
44
|
-
try {
|
|
45
|
-
await this.pluginLoader.callHook(instance.plugin, 'destroy', instance.context);
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
logger.error('PluginRegistryService', `Error destroying plugin ${pluginId}:`, error);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Remove from registry
|
|
52
|
-
this.plugins.delete(pluginId);
|
|
53
|
-
logger.info('PluginRegistryService', `Unloaded plugin: ${pluginId}`);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Enable a plugin
|
|
57
|
-
*/
|
|
58
|
-
async enablePlugin(pluginId) {
|
|
59
|
-
const instance = this.plugins.get(pluginId);
|
|
60
|
-
if (!instance) {
|
|
61
|
-
throw new Error(`Plugin ${pluginId} is not loaded`);
|
|
62
|
-
}
|
|
63
|
-
if (instance.enabled) {
|
|
64
|
-
logger.debug('PluginRegistryService', `Plugin ${pluginId} is already enabled`);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
// Call enable hook
|
|
68
|
-
if (instance.plugin.enable) {
|
|
69
|
-
await this.pluginLoader.callHook(instance.plugin, 'enable', instance.context);
|
|
70
|
-
}
|
|
71
|
-
instance.enabled = true;
|
|
72
|
-
instance.config.enabled = true;
|
|
73
|
-
this.savePluginState();
|
|
74
|
-
logger.info('PluginRegistryService', `Enabled plugin: ${pluginId}`);
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Disable a plugin
|
|
78
|
-
*/
|
|
79
|
-
async disablePlugin(pluginId) {
|
|
80
|
-
const instance = this.plugins.get(pluginId);
|
|
81
|
-
if (!instance) {
|
|
82
|
-
throw new Error(`Plugin ${pluginId} is not loaded`);
|
|
83
|
-
}
|
|
84
|
-
if (!instance.enabled) {
|
|
85
|
-
logger.debug('PluginRegistryService', `Plugin ${pluginId} is already disabled`);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
// Call disable hook
|
|
89
|
-
if (instance.plugin.disable) {
|
|
90
|
-
await this.pluginLoader.callHook(instance.plugin, 'disable', instance.context);
|
|
91
|
-
}
|
|
92
|
-
instance.enabled = false;
|
|
93
|
-
instance.config.enabled = false;
|
|
94
|
-
this.savePluginState();
|
|
95
|
-
logger.info('PluginRegistryService', `Disabled plugin: ${pluginId}`);
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Get a plugin instance
|
|
99
|
-
*/
|
|
100
|
-
getPlugin(pluginId) {
|
|
101
|
-
return this.plugins.get(pluginId);
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Get all plugins
|
|
105
|
-
*/
|
|
106
|
-
getAllPlugins() {
|
|
107
|
-
return [...this.plugins.values()];
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Get enabled plugins
|
|
111
|
-
*/
|
|
112
|
-
getEnabledPlugins() {
|
|
113
|
-
return this.getAllPlugins().filter(p => p.enabled);
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Check if a plugin is loaded
|
|
117
|
-
*/
|
|
118
|
-
isLoaded(pluginId) {
|
|
119
|
-
return this.plugins.has(pluginId);
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Check if a plugin is enabled
|
|
123
|
-
*/
|
|
124
|
-
isEnabled(pluginId) {
|
|
125
|
-
const plugin = this.plugins.get(pluginId);
|
|
126
|
-
return plugin?.enabled ?? false;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Get plugin permissions
|
|
130
|
-
*/
|
|
131
|
-
getPermissions(pluginId) {
|
|
132
|
-
return this.permissionsService.getPermissions(pluginId);
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Load all plugins from the plugins directory
|
|
136
|
-
*/
|
|
137
|
-
async loadAllPlugins() {
|
|
138
|
-
if (!existsSync(PLUGINS_DIR)) {
|
|
139
|
-
logger.info('PluginRegistryService', 'Plugins directory does not exist, skipping plugin loading');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true });
|
|
143
|
-
const pluginDirs = entries
|
|
144
|
-
.filter(entry => entry.isDirectory())
|
|
145
|
-
.map(entry => join(PLUGINS_DIR, entry.name));
|
|
146
|
-
logger.info('PluginRegistryService', `Found ${pluginDirs.length} potential plugin(s)`);
|
|
147
|
-
for (const pluginDir of pluginDirs) {
|
|
148
|
-
try {
|
|
149
|
-
const instance = await this.loadPlugin(pluginDir);
|
|
150
|
-
// Check if plugin was previously enabled
|
|
151
|
-
const savedState = this.getSavedPluginState(instance.manifest.id);
|
|
152
|
-
if (savedState?.enabled) {
|
|
153
|
-
await this.enablePlugin(instance.manifest.id);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
logger.error('PluginRegistryService', `Failed to load plugin from ${pluginDir}:`, error);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
logger.info('PluginRegistryService', `Loaded ${this.plugins.size} plugin(s)`);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Save plugin enabled/disabled state to config
|
|
164
|
-
*/
|
|
165
|
-
savePluginState() {
|
|
166
|
-
const pluginStates = {};
|
|
167
|
-
for (const [id, instance] of this.plugins) {
|
|
168
|
-
pluginStates[id] = { enabled: instance.enabled };
|
|
169
|
-
}
|
|
170
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
-
this.configService.set('pluginStates', pluginStates);
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Get saved plugin state from config
|
|
175
|
-
*/
|
|
176
|
-
getSavedPluginState(pluginId) {
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
-
const states = this.configService.get('pluginStates');
|
|
179
|
-
return states?.[pluginId];
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Clear all plugins
|
|
183
|
-
*/
|
|
184
|
-
async unloadAllPlugins() {
|
|
185
|
-
const pluginIds = [...this.plugins.keys()];
|
|
186
|
-
for (const pluginId of pluginIds) {
|
|
187
|
-
try {
|
|
188
|
-
await this.unloadPlugin(pluginId);
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
logger.error('PluginRegistryService', `Error unloading plugin ${pluginId}:`, error);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// Singleton instance
|
|
197
|
-
let instance = null;
|
|
198
|
-
/**
|
|
199
|
-
* Get the plugin registry service singleton
|
|
200
|
-
*/
|
|
201
|
-
export function getPluginRegistryService() {
|
|
202
|
-
if (!instance) {
|
|
203
|
-
instance = new PluginRegistryService();
|
|
204
|
-
}
|
|
205
|
-
return instance;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Reset the singleton (for testing)
|
|
209
|
-
*/
|
|
210
|
-
export function resetPluginRegistryService() {
|
|
211
|
-
if (instance) {
|
|
212
|
-
void instance.unloadAllPlugins();
|
|
213
|
-
}
|
|
214
|
-
instance = null;
|
|
215
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { logger } from "../logger/logger.service.js";
|
|
2
|
-
// Registry for plugin views
|
|
3
|
-
const pluginViews = new Map();
|
|
4
|
-
/**
|
|
5
|
-
* Register a plugin view
|
|
6
|
-
*/
|
|
7
|
-
export function registerPluginView(viewId, component) {
|
|
8
|
-
if (pluginViews.has(viewId)) {
|
|
9
|
-
logger.warn('PluginUIAPI', `View ${viewId} is already registered`);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
pluginViews.set(viewId, component);
|
|
13
|
-
logger.info('PluginUIAPI', `Registered view: ${viewId}`);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Unregister a plugin view
|
|
17
|
-
*/
|
|
18
|
-
export function unregisterPluginView(viewId) {
|
|
19
|
-
pluginViews.delete(viewId);
|
|
20
|
-
logger.info('PluginUIAPI', `Unregistered view: ${viewId}`);
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Get a plugin view by ID
|
|
24
|
-
*/
|
|
25
|
-
export function getPluginView(viewId) {
|
|
26
|
-
return pluginViews.get(viewId);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Check if a plugin view exists
|
|
30
|
-
*/
|
|
31
|
-
export function hasPluginView(viewId) {
|
|
32
|
-
return pluginViews.has(viewId);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Get all registered plugin views
|
|
36
|
-
*/
|
|
37
|
-
export function getAllPluginViews() {
|
|
38
|
-
return new Map(pluginViews);
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Clear all plugin views (for cleanup)
|
|
42
|
-
*/
|
|
43
|
-
export function clearAllPluginViews() {
|
|
44
|
-
pluginViews.clear();
|
|
45
|
-
logger.info('PluginUIAPI', 'Cleared all plugin views');
|
|
46
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { CONFIG_DIR } from "../../utils/constants.js";
|
|
2
|
-
import { logger } from "../logger/logger.service.js";
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { existsSync, mkdirSync, rmSync, cpSync, readdirSync } from 'node:fs';
|
|
5
|
-
import { execSync } from 'node:child_process';
|
|
6
|
-
const PLUGINS_DIR = join(CONFIG_DIR, 'plugins');
|
|
7
|
-
/**
|
|
8
|
-
* Plugin updater service
|
|
9
|
-
*/
|
|
10
|
-
class PluginUpdaterService {
|
|
11
|
-
/**
|
|
12
|
-
* Check if updates are available for a plugin
|
|
13
|
-
*/
|
|
14
|
-
async checkForUpdates(pluginId) {
|
|
15
|
-
const pluginDir = join(PLUGINS_DIR, pluginId);
|
|
16
|
-
if (!existsSync(pluginDir)) {
|
|
17
|
-
return { hasUpdate: false };
|
|
18
|
-
}
|
|
19
|
-
// Check if plugin is a git repository
|
|
20
|
-
const gitDir = join(pluginDir, '.git');
|
|
21
|
-
if (!existsSync(gitDir)) {
|
|
22
|
-
logger.warn('PluginUpdaterService', `Plugin ${pluginId} is not a git repository, cannot check for updates`);
|
|
23
|
-
return { hasUpdate: false };
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
// Fetch latest from remote
|
|
27
|
-
execSync('git fetch origin', {
|
|
28
|
-
cwd: pluginDir,
|
|
29
|
-
stdio: 'pipe',
|
|
30
|
-
windowsHide: true,
|
|
31
|
-
});
|
|
32
|
-
// Check if local is behind remote
|
|
33
|
-
const status = execSync('git status -uno', {
|
|
34
|
-
cwd: pluginDir,
|
|
35
|
-
stdio: 'pipe',
|
|
36
|
-
windowsHide: true,
|
|
37
|
-
}).toString();
|
|
38
|
-
const hasUpdate = status.includes('Your branch is behind');
|
|
39
|
-
// Get current version from manifest
|
|
40
|
-
const manifestPath = join(pluginDir, 'plugin.json');
|
|
41
|
-
let currentVersion = 'unknown';
|
|
42
|
-
if (existsSync(manifestPath)) {
|
|
43
|
-
const { readFileSync: fsReadFileSync } = await import('node:fs');
|
|
44
|
-
const manifest = fsReadFileSync(manifestPath, 'utf-8');
|
|
45
|
-
const parsedManifest = typeof manifest === 'string' ? JSON.parse(manifest) : manifest;
|
|
46
|
-
currentVersion = parsedManifest.version;
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
hasUpdate,
|
|
50
|
-
currentVersion,
|
|
51
|
-
latestVersion: hasUpdate ? 'available' : currentVersion,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
logger.error('PluginUpdaterService', `Failed to check updates for ${pluginId}:`, error);
|
|
56
|
-
return { hasUpdate: false };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Update a plugin with smart merge (preserve user data)
|
|
61
|
-
*/
|
|
62
|
-
async updatePlugin(pluginId) {
|
|
63
|
-
const pluginDir = join(PLUGINS_DIR, pluginId);
|
|
64
|
-
if (!existsSync(pluginDir)) {
|
|
65
|
-
return {
|
|
66
|
-
success: false,
|
|
67
|
-
error: `Plugin ${pluginId} is not installed`,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
// Get current version
|
|
72
|
-
const manifestPath = join(pluginDir, 'plugin.json');
|
|
73
|
-
let oldVersion = 'unknown';
|
|
74
|
-
if (existsSync(manifestPath)) {
|
|
75
|
-
const { readFileSync: fsReadFileSync } = await import('node:fs');
|
|
76
|
-
const manifest = fsReadFileSync(manifestPath, 'utf-8');
|
|
77
|
-
const parsedManifest = typeof manifest === 'string' ? JSON.parse(manifest) : manifest;
|
|
78
|
-
oldVersion = parsedManifest.version;
|
|
79
|
-
}
|
|
80
|
-
// Backup current version
|
|
81
|
-
const backupDir = join(pluginDir, '.backup');
|
|
82
|
-
if (existsSync(backupDir)) {
|
|
83
|
-
rmSync(backupDir, { recursive: true, force: true });
|
|
84
|
-
}
|
|
85
|
-
mkdirSync(backupDir, { recursive: true });
|
|
86
|
-
// Backup everything except data/ and config.json
|
|
87
|
-
const entries = readdirSync(pluginDir, { withFileTypes: true });
|
|
88
|
-
for (const entry of entries) {
|
|
89
|
-
if (entry.name === 'data' ||
|
|
90
|
-
entry.name === 'config.json' ||
|
|
91
|
-
entry.name === '.backup') {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const sourcePath = join(pluginDir, entry.name);
|
|
95
|
-
const targetPath = join(backupDir, entry.name);
|
|
96
|
-
if (entry.isDirectory()) {
|
|
97
|
-
cpSync(sourcePath, targetPath, { recursive: true });
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
cpSync(sourcePath, targetPath);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// Check if it's a git repository
|
|
104
|
-
const gitDir = join(pluginDir, '.git');
|
|
105
|
-
if (existsSync(gitDir)) {
|
|
106
|
-
// Git pull
|
|
107
|
-
try {
|
|
108
|
-
execSync('git pull origin main', {
|
|
109
|
-
cwd: pluginDir,
|
|
110
|
-
stdio: 'pipe',
|
|
111
|
-
windowsHide: true,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// Try master branch
|
|
116
|
-
execSync('git pull origin master', {
|
|
117
|
-
cwd: pluginDir,
|
|
118
|
-
stdio: 'pipe',
|
|
119
|
-
windowsHide: true,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
logger.warn('PluginUpdaterService', `Plugin ${pluginId} is not a git repository, cannot update`);
|
|
125
|
-
return {
|
|
126
|
-
success: false,
|
|
127
|
-
error: 'Plugin is not a git repository',
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
// Get new version
|
|
131
|
-
let newVersion = 'unknown';
|
|
132
|
-
if (existsSync(manifestPath)) {
|
|
133
|
-
const { readFileSync: fsReadFileSync } = await import('node:fs');
|
|
134
|
-
const manifest = fsReadFileSync(manifestPath, 'utf-8');
|
|
135
|
-
const parsedManifest = typeof manifest === 'string' ? JSON.parse(manifest) : manifest;
|
|
136
|
-
newVersion = parsedManifest.version;
|
|
137
|
-
}
|
|
138
|
-
// Install/update dependencies
|
|
139
|
-
const packageJsonPath = join(pluginDir, 'package.json');
|
|
140
|
-
if (existsSync(packageJsonPath)) {
|
|
141
|
-
try {
|
|
142
|
-
execSync('bun install', {
|
|
143
|
-
cwd: pluginDir,
|
|
144
|
-
stdio: 'pipe',
|
|
145
|
-
windowsHide: true,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
logger.warn('PluginUpdaterService', 'Failed to install dependencies:', error);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
logger.info('PluginUpdaterService', `Successfully updated ${pluginId} from ${oldVersion} to ${newVersion}`);
|
|
153
|
-
return {
|
|
154
|
-
success: true,
|
|
155
|
-
pluginId,
|
|
156
|
-
oldVersion,
|
|
157
|
-
newVersion,
|
|
158
|
-
message: `Updated from ${oldVersion} to ${newVersion}`,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
logger.error('PluginUpdaterService', `Failed to update ${pluginId}:`, error);
|
|
163
|
-
// Try to restore from backup
|
|
164
|
-
const backupDir = join(pluginDir, '.backup');
|
|
165
|
-
if (existsSync(backupDir)) {
|
|
166
|
-
logger.info('PluginUpdaterService', 'Restoring from backup...');
|
|
167
|
-
try {
|
|
168
|
-
const entries = readdirSync(backupDir, { withFileTypes: true });
|
|
169
|
-
for (const entry of entries) {
|
|
170
|
-
const sourcePath = join(backupDir, entry.name);
|
|
171
|
-
const targetPath = join(pluginDir, entry.name);
|
|
172
|
-
// Remove target first
|
|
173
|
-
if (existsSync(targetPath)) {
|
|
174
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
175
|
-
}
|
|
176
|
-
if (entry.isDirectory()) {
|
|
177
|
-
cpSync(sourcePath, targetPath, { recursive: true });
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
cpSync(sourcePath, targetPath);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
logger.info('PluginUpdaterService', 'Restored from backup');
|
|
184
|
-
}
|
|
185
|
-
catch (restoreError) {
|
|
186
|
-
logger.error('PluginUpdaterService', 'Failed to restore from backup:', restoreError);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
success: false,
|
|
191
|
-
error: `Update failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// Singleton instance
|
|
197
|
-
let instance = null;
|
|
198
|
-
/**
|
|
199
|
-
* Get the plugin updater service singleton
|
|
200
|
-
*/
|
|
201
|
-
export function getPluginUpdaterService() {
|
|
202
|
-
if (!instance) {
|
|
203
|
-
instance = new PluginUpdaterService();
|
|
204
|
-
}
|
|
205
|
-
return instance;
|
|
206
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// Scrobbling service — supports Last.fm and ListenBrainz
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { logger } from "../logger/logger.service.js";
|
|
4
|
-
// ---------- Last.fm ----------
|
|
5
|
-
async function lastfmScrobble(title, artist, timestamp, apiKey, sessionKey) {
|
|
6
|
-
const params = {
|
|
7
|
-
method: 'track.scrobble',
|
|
8
|
-
artist,
|
|
9
|
-
track: title,
|
|
10
|
-
timestamp: String(timestamp),
|
|
11
|
-
api_key: apiKey,
|
|
12
|
-
sk: sessionKey,
|
|
13
|
-
};
|
|
14
|
-
const secret = ''; // User must provide shared secret if using real auth
|
|
15
|
-
const sig = buildLastfmSignature(params, secret);
|
|
16
|
-
params['api_sig'] = sig;
|
|
17
|
-
params['format'] = 'json';
|
|
18
|
-
const body = new URLSearchParams(params);
|
|
19
|
-
const response = await fetch('https://ws.audioscrobbler.com/2.0/', {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
body,
|
|
22
|
-
});
|
|
23
|
-
if (!response.ok) {
|
|
24
|
-
throw new Error(`Last.fm scrobble failed: HTTP ${response.status}`);
|
|
25
|
-
}
|
|
26
|
-
const data = (await response.json());
|
|
27
|
-
if (data.error) {
|
|
28
|
-
throw new Error(`Last.fm error ${data.error}: ${data.message}`);
|
|
29
|
-
}
|
|
30
|
-
logger.info('ScrobblingService', 'Last.fm scrobble successful', {
|
|
31
|
-
title,
|
|
32
|
-
artist,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
function buildLastfmSignature(params, secret) {
|
|
36
|
-
const sorted = Object.keys(params)
|
|
37
|
-
.filter(k => k !== 'format' && k !== 'callback')
|
|
38
|
-
.sort()
|
|
39
|
-
.map(k => `${k}${params[k]}`)
|
|
40
|
-
.join('');
|
|
41
|
-
return createHash('sha256')
|
|
42
|
-
.update(sorted + secret)
|
|
43
|
-
.digest('hex');
|
|
44
|
-
}
|
|
45
|
-
// ---------- ListenBrainz ----------
|
|
46
|
-
async function listenbrainzScrobble(title, artist, listenedAt, token) {
|
|
47
|
-
const payload = {
|
|
48
|
-
listen_type: 'single',
|
|
49
|
-
payload: [
|
|
50
|
-
{
|
|
51
|
-
listened_at: listenedAt,
|
|
52
|
-
track_metadata: {
|
|
53
|
-
artist_name: artist,
|
|
54
|
-
track_name: title,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
|
-
const response = await fetch('https://api.listenbrainz.org/1/submit-listens', {
|
|
60
|
-
method: 'POST',
|
|
61
|
-
headers: {
|
|
62
|
-
Authorization: `Token ${token}`,
|
|
63
|
-
'Content-Type': 'application/json',
|
|
64
|
-
},
|
|
65
|
-
body: JSON.stringify(payload),
|
|
66
|
-
});
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
throw new Error(`ListenBrainz scrobble failed: HTTP ${response.status}`);
|
|
69
|
-
}
|
|
70
|
-
logger.info('ScrobblingService', 'ListenBrainz scrobble successful', {
|
|
71
|
-
title,
|
|
72
|
-
artist,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
// ---------- Main service ----------
|
|
76
|
-
export class ScrobblingService {
|
|
77
|
-
lastfmApiKey;
|
|
78
|
-
lastfmSessionKey;
|
|
79
|
-
listenbrainzToken;
|
|
80
|
-
configure(config) {
|
|
81
|
-
this.lastfmApiKey = config.lastfm?.apiKey;
|
|
82
|
-
this.lastfmSessionKey = config.lastfm?.sessionKey;
|
|
83
|
-
this.listenbrainzToken = config.listenbrainz?.token;
|
|
84
|
-
}
|
|
85
|
-
get isEnabled() {
|
|
86
|
-
return Boolean((this.lastfmApiKey && this.lastfmSessionKey) || this.listenbrainzToken);
|
|
87
|
-
}
|
|
88
|
-
async scrobble(track) {
|
|
89
|
-
if (!this.isEnabled)
|
|
90
|
-
return;
|
|
91
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
92
|
-
const tasks = [];
|
|
93
|
-
if (this.lastfmApiKey && this.lastfmSessionKey) {
|
|
94
|
-
tasks.push(lastfmScrobble(track.title, track.artist, timestamp, this.lastfmApiKey, this.lastfmSessionKey).catch(error => {
|
|
95
|
-
logger.error('ScrobblingService', 'Last.fm scrobble failed', {
|
|
96
|
-
error: error instanceof Error ? error.message : String(error),
|
|
97
|
-
});
|
|
98
|
-
}));
|
|
99
|
-
}
|
|
100
|
-
if (this.listenbrainzToken) {
|
|
101
|
-
tasks.push(listenbrainzScrobble(track.title, track.artist, timestamp, this.listenbrainzToken).catch(error => {
|
|
102
|
-
logger.error('ScrobblingService', 'ListenBrainz scrobble failed', {
|
|
103
|
-
error: error instanceof Error ? error.message : String(error),
|
|
104
|
-
});
|
|
105
|
-
}));
|
|
106
|
-
}
|
|
107
|
-
await Promise.all(tasks);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
let instance = null;
|
|
111
|
-
export const getScrobblingService = () => {
|
|
112
|
-
if (!instance)
|
|
113
|
-
instance = new ScrobblingService();
|
|
114
|
-
return instance;
|
|
115
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Sleep timer service - auto-stops playback after a set duration
|
|
2
|
-
import { logger } from "../logger/logger.service.js";
|
|
3
|
-
export const SLEEP_TIMER_PRESETS = [5, 10, 15, 30, 60];
|
|
4
|
-
class SleepTimerService {
|
|
5
|
-
static instance;
|
|
6
|
-
timer = null;
|
|
7
|
-
endTime = null;
|
|
8
|
-
constructor() { }
|
|
9
|
-
static getInstance() {
|
|
10
|
-
if (!SleepTimerService.instance) {
|
|
11
|
-
SleepTimerService.instance = new SleepTimerService();
|
|
12
|
-
}
|
|
13
|
-
return SleepTimerService.instance;
|
|
14
|
-
}
|
|
15
|
-
start(minutes, onExpire) {
|
|
16
|
-
this.cancel();
|
|
17
|
-
this.endTime = Date.now() + minutes * 60 * 1000;
|
|
18
|
-
logger.info('SleepTimerService', 'Timer started', { minutes });
|
|
19
|
-
this.timer = setTimeout(() => {
|
|
20
|
-
logger.info('SleepTimerService', 'Timer expired');
|
|
21
|
-
this.endTime = null;
|
|
22
|
-
this.timer = null;
|
|
23
|
-
onExpire();
|
|
24
|
-
}, minutes * 60 * 1000);
|
|
25
|
-
}
|
|
26
|
-
cancel() {
|
|
27
|
-
if (this.timer) {
|
|
28
|
-
clearTimeout(this.timer);
|
|
29
|
-
this.timer = null;
|
|
30
|
-
this.endTime = null;
|
|
31
|
-
logger.info('SleepTimerService', 'Timer cancelled');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/** Returns remaining seconds, or null if no timer active */
|
|
35
|
-
getRemainingSeconds() {
|
|
36
|
-
if (!this.endTime)
|
|
37
|
-
return null;
|
|
38
|
-
const remaining = Math.max(0, Math.ceil((this.endTime - Date.now()) / 1000));
|
|
39
|
-
return remaining;
|
|
40
|
-
}
|
|
41
|
-
isActive() {
|
|
42
|
-
return this.timer !== null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
export const getSleepTimerService = () => SleepTimerService.getInstance();
|