@involvex/youtube-music-cli 0.0.46 → 0.0.48

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