@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.
- package/CHANGELOG.md +2 -0
- package/dist/cli.js.map +3 -3
- 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 -89
- 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 -123
- 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 -507
- package/dist/source/services/web/web-streaming.service.js +0 -292
- 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 -789
- 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 -135
- package/dist/source/utils/format.js +0 -24
- package/dist/source/utils/icons.js +0 -28
- package/dist/source/utils/search-filters.js +0 -100
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { getYouTubeImportService } from "./youtube-import.service.js";
|
|
2
|
-
import { getSpotifyImportService } from "./spotify.service.js";
|
|
3
|
-
import { getTrackMatcherService } from "./track-matcher.service.js";
|
|
4
|
-
import { getConfigService } from "../config/config.service.js";
|
|
5
|
-
import { logger } from "../logger/logger.service.js";
|
|
6
|
-
// Helper to get track name from either Spotify or YouTube track
|
|
7
|
-
function getTrackName(track) {
|
|
8
|
-
return track.name || track.title;
|
|
9
|
-
}
|
|
10
|
-
class ImportService {
|
|
11
|
-
progressCallbacks = new Set();
|
|
12
|
-
currentImport = null;
|
|
13
|
-
/**
|
|
14
|
-
* Subscribe to import progress updates
|
|
15
|
-
*/
|
|
16
|
-
onProgress(callback) {
|
|
17
|
-
this.progressCallbacks.add(callback);
|
|
18
|
-
return () => {
|
|
19
|
-
this.progressCallbacks.delete(callback);
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Emit progress to all subscribers
|
|
24
|
-
*/
|
|
25
|
-
emitProgress(progress) {
|
|
26
|
-
for (const callback of this.progressCallbacks) {
|
|
27
|
-
callback(progress);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Create a unique playlist ID
|
|
32
|
-
*/
|
|
33
|
-
generatePlaylistId() {
|
|
34
|
-
return `import_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Save imported playlist to config
|
|
38
|
-
*/
|
|
39
|
-
savePlaylist(name, tracks) {
|
|
40
|
-
const configService = getConfigService();
|
|
41
|
-
const playlist = {
|
|
42
|
-
playlistId: this.generatePlaylistId(),
|
|
43
|
-
name,
|
|
44
|
-
tracks,
|
|
45
|
-
};
|
|
46
|
-
// Get existing playlists and add the new one
|
|
47
|
-
const existingPlaylists = configService.get('playlists') ?? [];
|
|
48
|
-
const updatedPlaylists = [...existingPlaylists, playlist];
|
|
49
|
-
configService.set('playlists', updatedPlaylists);
|
|
50
|
-
logger.info('ImportService', 'Playlist saved to config', {
|
|
51
|
-
playlistId: playlist.playlistId,
|
|
52
|
-
trackCount: tracks.length,
|
|
53
|
-
});
|
|
54
|
-
return playlist.playlistId;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Import a playlist from Spotify or YouTube
|
|
58
|
-
*/
|
|
59
|
-
async importPlaylist(source, urlOrId, customName, signal) {
|
|
60
|
-
const startTime = Date.now();
|
|
61
|
-
this.currentImport = { source, url: urlOrId, startTime };
|
|
62
|
-
// Initial progress
|
|
63
|
-
this.emitProgress({
|
|
64
|
-
status: 'fetching',
|
|
65
|
-
current: 0,
|
|
66
|
-
total: 0,
|
|
67
|
-
message: `Fetching ${source} playlist...`,
|
|
68
|
-
});
|
|
69
|
-
let originalTracks = [];
|
|
70
|
-
let playlistName = customName ?? `Imported ${source} playlist`;
|
|
71
|
-
try {
|
|
72
|
-
// Step 1: Fetch playlist
|
|
73
|
-
if (source === 'youtube') {
|
|
74
|
-
const youtubeService = getYouTubeImportService();
|
|
75
|
-
const playlist = await youtubeService.fetchPlaylist(urlOrId);
|
|
76
|
-
if (!playlist) {
|
|
77
|
-
throw new Error('Failed to fetch YouTube playlist. Please check the URL/ID.');
|
|
78
|
-
}
|
|
79
|
-
originalTracks = playlist.tracks;
|
|
80
|
-
playlistName = customName ?? playlist.name;
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
const spotifyService = getSpotifyImportService();
|
|
84
|
-
const playlist = await spotifyService.fetchPlaylist(urlOrId);
|
|
85
|
-
if (!playlist) {
|
|
86
|
-
throw new Error('Failed to fetch Spotify playlist. It may be private or invalid.');
|
|
87
|
-
}
|
|
88
|
-
if (playlist.tracks.length === 0) {
|
|
89
|
-
throw new Error('No tracks found. The playlist may be private or require authentication.');
|
|
90
|
-
}
|
|
91
|
-
originalTracks = playlist.tracks;
|
|
92
|
-
playlistName = customName ?? playlist.name;
|
|
93
|
-
}
|
|
94
|
-
const total = originalTracks.length;
|
|
95
|
-
// Check for abort
|
|
96
|
-
if (signal?.aborted) {
|
|
97
|
-
this.emitProgress({
|
|
98
|
-
status: 'cancelled',
|
|
99
|
-
current: 0,
|
|
100
|
-
total,
|
|
101
|
-
message: 'Import cancelled',
|
|
102
|
-
});
|
|
103
|
-
throw new Error('Import cancelled');
|
|
104
|
-
}
|
|
105
|
-
// Step 2: Match tracks
|
|
106
|
-
this.emitProgress({
|
|
107
|
-
status: 'matching',
|
|
108
|
-
current: 0,
|
|
109
|
-
total,
|
|
110
|
-
message: 'Matching tracks...',
|
|
111
|
-
});
|
|
112
|
-
const trackMatcher = getTrackMatcherService();
|
|
113
|
-
const matchedTracks = [];
|
|
114
|
-
const errors = [];
|
|
115
|
-
let matched = 0;
|
|
116
|
-
let failed = 0;
|
|
117
|
-
for (let i = 0; i < originalTracks.length; i++) {
|
|
118
|
-
// Check for abort
|
|
119
|
-
if (signal?.aborted) {
|
|
120
|
-
this.emitProgress({
|
|
121
|
-
status: 'cancelled',
|
|
122
|
-
current: i,
|
|
123
|
-
total,
|
|
124
|
-
currentTrack: originalTracks[i]
|
|
125
|
-
? getTrackName(originalTracks[i])
|
|
126
|
-
: undefined,
|
|
127
|
-
message: 'Import cancelled',
|
|
128
|
-
});
|
|
129
|
-
throw new Error('Import cancelled');
|
|
130
|
-
}
|
|
131
|
-
const originalTrack = originalTracks[i];
|
|
132
|
-
const trackName = getTrackName(originalTrack);
|
|
133
|
-
this.emitProgress({
|
|
134
|
-
status: 'matching',
|
|
135
|
-
current: i,
|
|
136
|
-
total,
|
|
137
|
-
currentTrack: trackName,
|
|
138
|
-
message: `Matching "${trackName}"...`,
|
|
139
|
-
});
|
|
140
|
-
const match = await trackMatcher.findMatch(originalTrack);
|
|
141
|
-
if (match.matchedTrack) {
|
|
142
|
-
matchedTracks.push(match.matchedTrack);
|
|
143
|
-
matched++;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
failed++;
|
|
147
|
-
const errorMsg = match.error
|
|
148
|
-
? `${trackName}: ${match.error}`
|
|
149
|
-
: `No match found for "${trackName}"`;
|
|
150
|
-
errors.push(errorMsg);
|
|
151
|
-
}
|
|
152
|
-
// Throttle progress updates (every 5 tracks or at the end)
|
|
153
|
-
if (i % 5 === 0 || i === total - 1) {
|
|
154
|
-
this.emitProgress({
|
|
155
|
-
status: 'matching',
|
|
156
|
-
current: i + 1,
|
|
157
|
-
total,
|
|
158
|
-
currentTrack: trackName,
|
|
159
|
-
message: `Matched ${matched}/${total} tracks`,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Step 3: Create playlist
|
|
164
|
-
this.emitProgress({
|
|
165
|
-
status: 'creating',
|
|
166
|
-
current: total,
|
|
167
|
-
total,
|
|
168
|
-
message: 'Creating playlist...',
|
|
169
|
-
});
|
|
170
|
-
const playlistId = this.savePlaylist(playlistName, matchedTracks);
|
|
171
|
-
// Final progress
|
|
172
|
-
this.emitProgress({
|
|
173
|
-
status: 'completed',
|
|
174
|
-
current: total,
|
|
175
|
-
total,
|
|
176
|
-
message: `Import completed: ${matched} tracks matched`,
|
|
177
|
-
});
|
|
178
|
-
const result = {
|
|
179
|
-
playlistId,
|
|
180
|
-
playlistName,
|
|
181
|
-
source,
|
|
182
|
-
total,
|
|
183
|
-
matched,
|
|
184
|
-
failed,
|
|
185
|
-
matches: [], // Could be populated if needed for detailed results
|
|
186
|
-
errors,
|
|
187
|
-
duration: Date.now() - startTime,
|
|
188
|
-
};
|
|
189
|
-
logger.info('ImportService', 'Import completed', {
|
|
190
|
-
playlistId,
|
|
191
|
-
playlistName,
|
|
192
|
-
source,
|
|
193
|
-
total,
|
|
194
|
-
matched,
|
|
195
|
-
failed,
|
|
196
|
-
duration: result.duration,
|
|
197
|
-
});
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
-
logger.error('ImportService', 'Import failed', {
|
|
203
|
-
source,
|
|
204
|
-
url: urlOrId,
|
|
205
|
-
error: message,
|
|
206
|
-
});
|
|
207
|
-
if (message === 'Import cancelled') {
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
210
|
-
this.emitProgress({
|
|
211
|
-
status: 'failed',
|
|
212
|
-
current: 0,
|
|
213
|
-
total: originalTracks.length,
|
|
214
|
-
message: `Import failed: ${message}`,
|
|
215
|
-
});
|
|
216
|
-
throw error;
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
this.currentImport = null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Validate a playlist URL/ID before importing
|
|
224
|
-
*/
|
|
225
|
-
async validatePlaylist(source, urlOrId) {
|
|
226
|
-
try {
|
|
227
|
-
if (source === 'youtube') {
|
|
228
|
-
const service = getYouTubeImportService();
|
|
229
|
-
return await service.validatePlaylist(urlOrId);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
const service = getSpotifyImportService();
|
|
233
|
-
return await service.validatePlaylist(urlOrId);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Get current import status
|
|
242
|
-
*/
|
|
243
|
-
getCurrentImport() {
|
|
244
|
-
if (!this.currentImport)
|
|
245
|
-
return null;
|
|
246
|
-
return {
|
|
247
|
-
source: this.currentImport.source,
|
|
248
|
-
url: this.currentImport.url,
|
|
249
|
-
elapsed: Date.now() - this.currentImport.startTime,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Cancel the current import
|
|
254
|
-
*/
|
|
255
|
-
cancelImport() {
|
|
256
|
-
this.currentImport = null;
|
|
257
|
-
this.emitProgress({
|
|
258
|
-
status: 'cancelled',
|
|
259
|
-
current: 0,
|
|
260
|
-
total: 0,
|
|
261
|
-
message: 'Import cancelled',
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// Singleton instance
|
|
266
|
-
let importServiceInstance = null;
|
|
267
|
-
export function getImportService() {
|
|
268
|
-
if (!importServiceInstance) {
|
|
269
|
-
importServiceInstance = new ImportService();
|
|
270
|
-
}
|
|
271
|
-
return importServiceInstance;
|
|
272
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { logger } from "../logger/logger.service.js";
|
|
2
|
-
class SpotifyImportService {
|
|
3
|
-
OEMBED_URL = 'https://open.spotify.com/oembed';
|
|
4
|
-
/**
|
|
5
|
-
* Extract playlist ID from various Spotify URL formats
|
|
6
|
-
*/
|
|
7
|
-
extractPlaylistId(input) {
|
|
8
|
-
// Direct playlist ID (base62 string, typically 22 characters)
|
|
9
|
-
if (/^[A-Za-z0-9]{22}$/.test(input)) {
|
|
10
|
-
return input;
|
|
11
|
-
}
|
|
12
|
-
// spotify:playlist:ID format
|
|
13
|
-
const uriMatch = input.match(/spotify:playlist:([A-Za-z0-9]+)/);
|
|
14
|
-
if (uriMatch) {
|
|
15
|
-
return uriMatch[1] ?? null;
|
|
16
|
-
}
|
|
17
|
-
// open.spotify.com/playlist/ID format
|
|
18
|
-
const urlMatch = input.match(/open\.spotify\.com\/playlist\/([A-Za-z0-9]+)/);
|
|
19
|
-
if (urlMatch) {
|
|
20
|
-
return urlMatch[1] ?? null;
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Build a Spotify playlist URL from ID
|
|
26
|
-
*/
|
|
27
|
-
buildPlaylistUrl(playlistId) {
|
|
28
|
-
return `https://open.spotify.com/playlist/${playlistId}`;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Fetch playlist metadata using Spotify oEmbed API
|
|
32
|
-
* This works for public playlists without authentication
|
|
33
|
-
*/
|
|
34
|
-
async fetchPlaylistMetadata(urlOrId) {
|
|
35
|
-
const playlistId = this.extractPlaylistId(urlOrId);
|
|
36
|
-
if (!playlistId) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
const playlistUrl = this.buildPlaylistUrl(playlistId);
|
|
40
|
-
const oembedUrl = `${this.OEMBED_URL}?url=${encodeURIComponent(playlistUrl)}`;
|
|
41
|
-
try {
|
|
42
|
-
logger.debug('SpotifyImportService', 'Fetching oEmbed metadata', {
|
|
43
|
-
playlistId,
|
|
44
|
-
});
|
|
45
|
-
const response = await fetch(oembedUrl);
|
|
46
|
-
if (!response.ok) {
|
|
47
|
-
logger.warn('SpotifyImportService', 'oEmbed request failed', {
|
|
48
|
-
status: response.status,
|
|
49
|
-
playlistId,
|
|
50
|
-
});
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
const data = (await response.json());
|
|
54
|
-
return {
|
|
55
|
-
title: data.title || 'Unknown Playlist',
|
|
56
|
-
url: playlistUrl,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
logger.error('SpotifyImportService', 'oEmbed fetch failed', {
|
|
61
|
-
playlistId,
|
|
62
|
-
error: error instanceof Error ? error.message : String(error),
|
|
63
|
-
});
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Fetch playlist tracks by scraping the Spotify Web API
|
|
69
|
-
* Note: This method has limitations and may not work for private playlists
|
|
70
|
-
*/
|
|
71
|
-
async fetchPlaylistTracks(playlistId) {
|
|
72
|
-
const metadata = await this.fetchPlaylistMetadata(playlistId);
|
|
73
|
-
if (!metadata) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
// Use Spotify's Web API endpoint for playlist tracks (no auth required for public playlists)
|
|
78
|
-
const apiUrl = `https://api.spotify.com/v1/playlists/${playlistId}/tracks?limit=100`;
|
|
79
|
-
logger.debug('SpotifyImportService', 'Fetching tracks from API', {
|
|
80
|
-
playlistId,
|
|
81
|
-
});
|
|
82
|
-
const response = await fetch(apiUrl, {
|
|
83
|
-
headers: {
|
|
84
|
-
Accept: 'application/json',
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
// API may require auth for some playlists
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
if (response.status === 401 || response.status === 403) {
|
|
90
|
-
logger.warn('SpotifyImportService', 'Playlist requires authentication', {
|
|
91
|
-
playlistId,
|
|
92
|
-
status: response.status,
|
|
93
|
-
});
|
|
94
|
-
return this.createPartialPlaylist(playlistId, metadata);
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
const data = (await response.json());
|
|
99
|
-
const tracks = data.items
|
|
100
|
-
?.filter(item => item.track)
|
|
101
|
-
.map(item => ({
|
|
102
|
-
id: item.track.id ?? '',
|
|
103
|
-
name: item.track.name ?? 'Unknown Track',
|
|
104
|
-
artists: item.track.artists?.map(a => a.name ?? 'Unknown Artist') ?? [],
|
|
105
|
-
album: item.track.album?.name,
|
|
106
|
-
duration: Math.round((item.track.duration_ms ?? 0) / 1000),
|
|
107
|
-
})) ?? [];
|
|
108
|
-
return {
|
|
109
|
-
id: playlistId,
|
|
110
|
-
name: metadata.title,
|
|
111
|
-
tracks,
|
|
112
|
-
isPublic: true,
|
|
113
|
-
url: metadata.url,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
logger.error('SpotifyImportService', 'Failed to fetch playlist', {
|
|
118
|
-
playlistId,
|
|
119
|
-
error: error instanceof Error ? error.message : String(error),
|
|
120
|
-
});
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Create a partial playlist when only metadata is available
|
|
126
|
-
* This is used when authentication is required but we have basic info
|
|
127
|
-
*/
|
|
128
|
-
createPartialPlaylist(playlistId, metadata) {
|
|
129
|
-
logger.info('SpotifyImportService', 'Creating partial playlist (auth required)', {
|
|
130
|
-
playlistId,
|
|
131
|
-
});
|
|
132
|
-
return {
|
|
133
|
-
id: playlistId,
|
|
134
|
-
name: metadata.title,
|
|
135
|
-
tracks: [],
|
|
136
|
-
isPublic: false,
|
|
137
|
-
url: metadata.url,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Fetch a Spotify playlist (with graceful degradation)
|
|
142
|
-
*/
|
|
143
|
-
async fetchPlaylist(urlOrId) {
|
|
144
|
-
const playlistId = this.extractPlaylistId(urlOrId);
|
|
145
|
-
if (!playlistId) {
|
|
146
|
-
logger.warn('SpotifyImportService', 'Invalid Spotify playlist URL or ID', {
|
|
147
|
-
input: urlOrId,
|
|
148
|
-
});
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
logger.info('SpotifyImportService', 'Fetching Spotify playlist', {
|
|
152
|
-
playlistId,
|
|
153
|
-
});
|
|
154
|
-
return this.fetchPlaylistTracks(playlistId);
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Validate if a playlist is accessible
|
|
158
|
-
*/
|
|
159
|
-
async validatePlaylist(urlOrId) {
|
|
160
|
-
const playlist = await this.fetchPlaylist(urlOrId);
|
|
161
|
-
return playlist !== null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// Singleton instance
|
|
165
|
-
let spotifyImportServiceInstance = null;
|
|
166
|
-
export function getSpotifyImportService() {
|
|
167
|
-
if (!spotifyImportServiceInstance) {
|
|
168
|
-
spotifyImportServiceInstance = new SpotifyImportService();
|
|
169
|
-
}
|
|
170
|
-
return spotifyImportServiceInstance;
|
|
171
|
-
}
|