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