@song-spotlight/api 1.3.2 → 2.0.1

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/README.md CHANGED
@@ -45,19 +45,15 @@ From `@song-spotlight/api/handlers`
45
45
 
46
46
  ### `parseLink(link: string): Promise<Song?>`
47
47
 
48
- Tries to parse the provided **link**. Returns a **Song** if successful, or `null` if nothing was found. Either response is temporarily cached.
49
-
50
- ### `rebuildLink(song: Song): Promise<string?>`
51
-
52
- Tries to recreate the link to the provided **Song**. Returns `string` if successful, or `null` if nothing was found. Either response is temporarily cached.
48
+ Tries to parse the provided **link**. Returns a **Song** if successful, or `null` if nothing was found. Either response is cached until `clearCache` is called.
53
49
 
54
50
  ### `renderSong(song: Song): Promise<RenderSongInfo?>`
55
51
 
56
- Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is temporarily cached.
52
+ Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is cached until `clearCache` is called.
57
53
 
58
54
  ### `validateSong(song: Song): Promise<boolean>`
59
55
 
60
- Validates if the provided **Song** exists. Returns a `boolean` depending on if the check was successful or not. Either response is temporarily cached.
56
+ Validates if the provided **Song** exists. Returns a `boolean` depending on if the check was successful or not. Either response is cached until `clearCache` is called.
61
57
 
62
58
  ### `clearCache()`
63
59
 
@@ -92,3 +88,30 @@ import { net } from "electron";
92
88
 
93
89
  setFetchHandler(net.fetch as unknown as typeof fetch);
94
90
  ```
91
+
92
+ ### `isListLayout(song: Song, render?: RenderSongInfo): boolean`
93
+
94
+ Returns whether the specified **Song** should have a tall layout (for **playlists**, **albums** and **artists**) or a short layout (for **tracks**).
95
+
96
+ ```ts
97
+ isListLayout({ service: "soundcloud", type: "user", id: "914653456" });
98
+ // true
99
+ ```
100
+
101
+ ### `getServiceLabel(service: string): string?`
102
+
103
+ Loops through all **services** and returns the corresponding **service**'s label.
104
+
105
+ ```ts
106
+ getServiceLabel("applemusic");
107
+ // "Apple Music"
108
+ ```
109
+
110
+ ### `sid(song: Song): string`
111
+
112
+ Helper function which stringifies a **Song**, useful for caching or for using as keys.
113
+
114
+ ```ts
115
+ sid({ service: "soundcloud", type: "user", id: "914653456" });
116
+ // "soundcloud:user:914653456"
117
+ ```
@@ -1,104 +1,10 @@
1
- import { PLAYLIST_LIMIT, clean, parseNextData, request } from "./common-DrSlxvDY.js";
1
+ import { c as request, o as PLAYLIST_LIMIT, r as parseLink, s as parseNextData, t as $ } from "./finders-CDZyaavx.js";
2
2
 
3
- //#region src/handlers/finders.ts
4
- const $ = {
5
- services: [],
6
- parsers: []
7
- };
8
- function sid(song) {
9
- return [
10
- song.service,
11
- song.type,
12
- song.id
13
- ].join(":");
14
- }
15
- const parseCache = /* @__PURE__ */ new Map();
16
- const validateCache = /* @__PURE__ */ new Map();
17
- /**
18
- * Tries to parse the provided **link**. Returns a **Song** if successful, or `null` if nothing was found. Either response is temporarily cached.
19
- * @example ```ts
20
- * await parseLink("https://soundcloud.com/c0ncernn");
21
- * // { service: "soundcloud", type: "user", id: "914653456" }
22
- * ```
23
- */
24
- async function parseLink(link) {
25
- const cleaned = clean(link);
26
- if (parseCache.has(cleaned)) return parseCache.get(cleaned);
27
- const { hostname, pathname } = new URL(cleaned);
28
- const path = pathname.slice(1).split(/\/+/);
29
- let song = null;
30
- for (const parser of $.parsers) if (parser.hosts.includes(hostname)) {
31
- song = await parser.parse(cleaned, hostname, path);
32
- if (song) break;
33
- }
34
- parseCache.set(cleaned, song);
35
- if (song) validateCache.set(sid(song), true);
36
- return song;
37
- }
38
- const linkCache = /* @__PURE__ */ new Map();
39
- /**
40
- * Tries to recreate the link to the provided **Song**. Returns `string` if successful, or `null` if nothing was found. Either response is temporarily cached.
41
- * @example ```ts
42
- * await parseLink({ service: "soundcloud", type: "user", id: "914653456" });
43
- * // https://soundcloud.com/c0ncernn
44
- * ```
45
- */
46
- async function rebuildLink(song) {
47
- const id = sid(song);
48
- if (linkCache.has(id)) return linkCache.get(id);
49
- let link = null;
50
- const service = $.services.find((x) => x.name === song.service);
51
- if (service?.types.includes(song.type)) link = await service.rebuild(song.type, song.id);
52
- linkCache.set(id, link);
53
- if (link) validateCache.set(id, true);
54
- return link;
55
- }
56
- const renderCache = /* @__PURE__ */ new Map();
57
- /**
58
- * Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is temporarily cached.
59
- * @example ```ts
60
- * await renderSong({ service: "soundcloud", type: "user", id: "914653456" });
61
- * // { label: "leroy", sublabel: "Top tracks", explicit: false, form: "list", ... }
62
- * ```
63
- */
64
- async function renderSong(song) {
65
- const id = sid(song);
66
- if (renderCache.has(id)) return renderCache.get(id);
67
- let info = null;
68
- const service = $.services.find((x) => x.name === song.service);
69
- if (service?.types.includes(song.type)) info = await service.render(song.type, song.id);
70
- renderCache.set(id, info);
71
- if (song) validateCache.set(sid(song), true);
72
- return info;
73
- }
74
- /**
75
- * Validates if the provided **Song** exists. Returns a `boolean` depending on if the check was successful or not. Either response is temporarily cached.
76
- * @example ```ts
77
- * await renderSong({ service: "soundcloud", type: "user", id: "914653456" });
78
- * // true
79
- * ```
80
- */
81
- async function validateSong(song) {
82
- const id = sid(song);
83
- if (validateCache.has(id)) return validateCache.get(id);
84
- let valid = false;
85
- const service = $.services.find((x) => x.name === song.service);
86
- if (service?.types.includes(song.type)) valid = await service.validate(song.type, song.id);
87
- validateCache.set(id, valid);
88
- return valid;
89
- }
90
- /** Clears the cache for all handler functions */
91
- function clearCache() {
92
- parseCache.clear();
93
- linkCache.clear();
94
- renderCache.clear();
95
- validateCache.clear();
96
- }
97
-
98
- //#endregion
99
3
  //#region src/handlers/defs/parsers/songdotlink.ts
4
+ const alphabeticRegex = /^[^-_][a-z0-9-_]+[^-_]$/i;
100
5
  const songdotlink = {
101
6
  name: "song.link",
7
+ label: "song.link",
102
8
  hosts: [
103
9
  "song.link",
104
10
  "album.link",
@@ -111,8 +17,8 @@ const songdotlink = {
111
17
  async parse(link, _host, path) {
112
18
  const [first, second, third] = path;
113
19
  if (!first || third) return null;
114
- if (second && Number.isNaN(+second)) return null;
115
- else if (!second && (!first.match(/^[A-z0-9-_]+$/) || first.match(/^[-_]/) || first.match(/[-_]$/))) return null;
20
+ if (second && !alphabeticRegex.test(second)) return null;
21
+ else if (!second && !alphabeticRegex.test(first)) return null;
116
22
  const html = (await request({ url: link })).text;
117
23
  const sections = parseNextData(html)?.props?.pageProps?.pageData?.sections;
118
24
  if (!sections) return null;
@@ -144,16 +50,18 @@ function makeCache(name, retrieve) {
144
50
  //#endregion
145
51
  //#region src/handlers/defs/services/applemusic.ts
146
52
  const geo = "us", defaultName = "songspotlight";
53
+ function applemusicLink(type, id) {
54
+ return `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`;
55
+ }
147
56
  const applemusicToken = makeCache("applemusicToken", async (html) => {
148
57
  html ??= (await request({ url: `https://music.apple.com/${geo}/new` })).text;
149
58
  const asset = html.match(/src="(\/assets\/index~\w+\.js)"/i)?.[1];
150
59
  if (!asset) return;
151
- const js = (await request({ url: `https://music.apple.com${asset}` })).text;
152
- const code = js.match(/\w+="(ey.*?)"/i)?.[1];
153
- return code;
60
+ return (await request({ url: `https://music.apple.com${asset}` })).text.match(/\w+="(ey.*?)"/i)?.[1];
154
61
  });
155
62
  const applemusic = {
156
63
  name: "applemusic",
64
+ label: "Apple Music",
157
65
  hosts: ["music.apple.com", "geo.music.apple.com"],
158
66
  types: [
159
67
  "artist",
@@ -164,7 +72,7 @@ const applemusic = {
164
72
  async parse(_link, _host, path) {
165
73
  const [country, type, name, id, fourth] = path;
166
74
  if (!country || !type || !this.types.includes(type) || !name || !id || fourth) return null;
167
- const res = await request({ url: this.rebuild(type, id) });
75
+ const res = await request({ url: applemusicLink(type, id) });
168
76
  if (res.status !== 200) return null;
169
77
  await applemusicToken.retrieve(res.text);
170
78
  return {
@@ -177,7 +85,11 @@ const applemusic = {
177
85
  const token = await applemusicToken.retrieve();
178
86
  if (!token) return null;
179
87
  const res = await request({
180
- url: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s/${id}?include=songs`,
88
+ url: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s`,
89
+ query: {
90
+ include: "songs",
91
+ ids: id
92
+ },
181
93
  headers: {
182
94
  authorization: `Bearer ${token}`,
183
95
  origin: "https://music.apple.com"
@@ -187,51 +99,43 @@ const applemusic = {
187
99
  const { attributes, relationships } = res.json.data[0];
188
100
  const base = {
189
101
  label: attributes.name,
190
- sublabel: attributes.artistName ?? "Top Songs",
102
+ sublabel: attributes.artistName ?? "Top songs",
103
+ link: attributes.url,
191
104
  explicit: attributes.contentRating === "explicit"
192
105
  };
193
106
  const thumbnailUrl = attributes.artwork?.url?.replace(/{[wh]}/g, "128");
194
107
  if (type === "song") {
195
108
  const duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;
196
109
  return {
197
- ...base,
198
110
  form: "single",
111
+ ...base,
199
112
  thumbnailUrl,
200
- single: {
201
- audio: previewUrl && duration ? {
202
- previewUrl,
203
- duration
204
- } : void 0,
205
- link: attributes.url
206
- }
113
+ single: { audio: previewUrl && duration ? {
114
+ previewUrl,
115
+ duration
116
+ } : void 0 }
207
117
  };
208
- }
209
- const songs = (relationships.songs ?? relationships.tracks)?.data;
210
- if (!songs) return null;
211
- return {
212
- ...base,
118
+ } else return {
213
119
  form: "list",
120
+ ...base,
214
121
  thumbnailUrl,
215
- list: songs.slice(0, PLAYLIST_LIMIT).map(({ attributes: song }) => {
216
- const duration = song.durationInMillis, previewUrl = song.previews?.[0]?.url;
122
+ list: (relationships.tracks?.data ?? relationships.songs?.data ?? []).slice(0, PLAYLIST_LIMIT).map(({ attributes }) => {
123
+ const duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;
217
124
  return {
218
- label: song.name,
219
- sublabel: song.artistName,
220
- explicit: song.contentRating === "explicit",
125
+ label: attributes.name,
126
+ sublabel: attributes.artistName,
127
+ link: attributes.url,
128
+ explicit: attributes.contentRating === "explicit",
221
129
  audio: previewUrl && duration ? {
222
130
  previewUrl,
223
131
  duration
224
- } : void 0,
225
- link: song.url
132
+ } : void 0
226
133
  };
227
134
  })
228
135
  };
229
136
  },
230
137
  async validate(type, id) {
231
- return (await request({ url: this.rebuild(type, id) })).status === 200;
232
- },
233
- rebuild(type, id) {
234
- return `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`;
138
+ return (await request({ url: applemusicLink(type, id) })).status === 200;
235
139
  }
236
140
  };
237
141
 
@@ -240,12 +144,12 @@ const applemusic = {
240
144
  const client_id = "nIjtjiYnjkOhMyh5xrbqEW12DxeJVnic";
241
145
  async function parseWidget(type, id, tracks) {
242
146
  return (await request({
243
- url: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? "/tracks?limit=20" : ""}`,
147
+ url: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? "/tracks" : ""}`,
244
148
  query: {
245
- client_id,
246
- app_version: "1764154491",
247
149
  format: "json",
248
- representation: "full"
150
+ client_id,
151
+ app_version: "1768986291",
152
+ limit: "20"
249
153
  }
250
154
  })).json;
251
155
  }
@@ -269,6 +173,7 @@ async function parsePreview(transcodings) {
269
173
  }
270
174
  const soundcloud = {
271
175
  name: "soundcloud",
176
+ label: "Soundcloud",
272
177
  hosts: [
273
178
  "soundcloud.com",
274
179
  "m.soundcloud.com",
@@ -317,54 +222,40 @@ const soundcloud = {
317
222
  const base = {
318
223
  label: data.title ?? data.username,
319
224
  sublabel: data.user?.username ?? "Top tracks",
225
+ link: data.permalink_url,
320
226
  explicit: Boolean(data.publisher_metadata?.explicit)
321
227
  };
322
228
  const thumbnailUrl = data.artwork_url ?? data.avatar_url;
323
229
  if (type === "track") {
324
- const audio = await parsePreview(data.media?.transcodings ?? []);
230
+ const audio = await parsePreview(data.media?.transcodings ?? []).catch(() => void 0);
325
231
  return {
326
- ...base,
327
232
  form: "single",
233
+ ...base,
328
234
  thumbnailUrl,
329
- single: {
330
- audio,
331
- link: data.permalink_url
332
- }
235
+ single: { audio }
333
236
  };
334
- }
335
- let tracks = [];
336
- if (type === "user") {
337
- const got = await parseWidget(type, id, true);
338
- if (!got?.collection) return null;
339
- tracks = got.collection;
340
237
  } else {
341
- if (!data.tracks) return null;
342
- tracks = data.tracks;
343
- }
344
- const list = [];
345
- for (const track of tracks) {
346
- if (!track.title || list.length >= PLAYLIST_LIMIT) continue;
347
- const audio = await parsePreview(track.media?.transcodings ?? []);
348
- list.push({
349
- label: track.title,
350
- sublabel: track.user?.username ?? "IDKK",
351
- explicit: Boolean(track.publisher_metadata.explicit),
352
- audio,
353
- link: track.permalink_url
354
- });
238
+ let tracks = [];
239
+ if (type === "user") {
240
+ const got = await parseWidget(type, id, true).catch(() => void 0);
241
+ if (got?.collection) tracks = got.collection;
242
+ } else if (data.tracks) tracks = data.tracks;
243
+ return {
244
+ form: "list",
245
+ ...base,
246
+ thumbnailUrl,
247
+ list: await Promise.all(tracks.filter((x) => x.title).slice(0, PLAYLIST_LIMIT).map(async (track) => ({
248
+ label: track.title,
249
+ sublabel: track.user?.username ?? "unknown",
250
+ link: track.permalink_url,
251
+ explicit: Boolean(track.publisher_metadata.explicit),
252
+ audio: await parsePreview(track.media?.transcodings ?? []).catch(() => void 0)
253
+ })))
254
+ };
355
255
  }
356
- return {
357
- ...base,
358
- form: "list",
359
- thumbnailUrl,
360
- list
361
- };
362
256
  },
363
257
  async validate(type, id) {
364
258
  return (await parseWidget(type, id, false))?.id !== void 0;
365
- },
366
- async rebuild(type, id) {
367
- return (await parseWidget(type, id, false))?.permalink_url ?? null;
368
259
  }
369
260
  };
370
261
 
@@ -380,6 +271,7 @@ function fromUri(uri) {
380
271
  }
381
272
  const spotify = {
382
273
  name: "spotify",
274
+ label: "Spotify",
383
275
  hosts: ["open.spotify.com"],
384
276
  types: [
385
277
  "track",
@@ -403,52 +295,37 @@ const spotify = {
403
295
  const base = {
404
296
  label: data.title,
405
297
  sublabel: data.subtitle ?? data.artists?.map((x) => x.name).join(", "),
298
+ link: fromUri(data.uri),
406
299
  explicit: Boolean(data.isExplicit)
407
300
  };
408
301
  const thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]?.url.replace(/:\/\/.*?\.spotifycdn\.com\/image/, "://i.scdn.co/image");
409
- if (type === "track") {
410
- const link = fromUri(data.uri);
411
- return {
412
- ...base,
413
- form: "single",
414
- thumbnailUrl,
415
- single: {
416
- audio: data.audioPreview && data.duration ? {
417
- duration: data.duration,
418
- previewUrl: data.audioPreview.url
419
- } : void 0,
420
- link
421
- }
422
- };
423
- } else {
424
- const list = [];
425
- for (const track of data.trackList ?? []) {
426
- if (list.length >= PLAYLIST_LIMIT) continue;
427
- const link = fromUri(track.uri);
428
- list.push({
429
- label: track.title,
430
- sublabel: track.subtitle ?? track.artists?.map((x) => x.name).join(", "),
431
- explicit: Boolean(track.isExplicit),
432
- audio: track.audioPreview && track.duration ? {
433
- duration: track.duration,
434
- previewUrl: track.audioPreview.url
435
- } : void 0,
436
- link
437
- });
438
- }
439
- return {
440
- ...base,
441
- form: "list",
442
- thumbnailUrl,
443
- list
444
- };
445
- }
302
+ if (type === "track") return {
303
+ form: "single",
304
+ ...base,
305
+ thumbnailUrl,
306
+ single: { audio: data.audioPreview && data.duration ? {
307
+ duration: data.duration,
308
+ previewUrl: data.audioPreview.url
309
+ } : void 0 }
310
+ };
311
+ else return {
312
+ form: "list",
313
+ ...base,
314
+ thumbnailUrl,
315
+ list: (data.trackList ?? []).slice(0, PLAYLIST_LIMIT).map((track) => ({
316
+ label: track.title,
317
+ sublabel: track.subtitle ?? track.artists?.map((x) => x.name).join(", "),
318
+ link: fromUri(track.uri),
319
+ explicit: Boolean(track.isExplicit),
320
+ audio: track.audioPreview && track.duration ? {
321
+ duration: track.duration,
322
+ previewUrl: track.audioPreview.url
323
+ } : void 0
324
+ }))
325
+ };
446
326
  },
447
327
  async validate(type, id) {
448
328
  return !(await parseEmbed(type, id))?.props?.pageProps?.title;
449
- },
450
- rebuild(type, id) {
451
- return `https://open.spotify.com/${type}/${id}`;
452
329
  }
453
330
  };
454
331
 
@@ -464,4 +341,4 @@ const parsers = [songdotlink, ...services];
464
341
  $.parsers = parsers;
465
342
 
466
343
  //#endregion
467
- export { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
344
+ export { services as n, parsers as t };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-CGspipcB.js","names":[],"sources":["../src/handlers/defs/parsers/songdotlink.ts","../src/handlers/defs/cache.ts","../src/handlers/defs/services/applemusic.ts","../src/handlers/defs/services/soundcloud.ts","../src/handlers/defs/services/spotify.ts","../src/handlers/core.ts"],"sourcesContent":["import { parseNextData, request } from \"handlers/common\";\nimport { parseLink } from \"handlers/finders\";\nimport { type SongParser } from \"handlers/helpers\";\n\ninterface Next {\n\tprops: {\n\t\tpageProps: {\n\t\t\tpageData: {\n\t\t\t\tsections: {\n\t\t\t\t\tlinks: {\n\t\t\t\t\t\tplatform: string;\n\t\t\t\t\t\turl: string;\n\t\t\t\t\t}[];\n\t\t\t\t}[];\n\t\t\t};\n\t\t};\n\t};\n}\n\nconst alphabeticRegex = /^[^-_][a-z0-9-_]+[^-_]$/i;\n\nexport const songdotlink: SongParser = {\n\tname: \"song.link\",\n\tlabel: \"song.link\",\n\thosts: [\n\t\t\"song.link\",\n\t\t\"album.link\",\n\t\t\"artist.link\",\n\t\t\"pods.link\",\n\t\t\"playlist.link\",\n\t\t\"mylink.page\",\n\t\t\"odesli.co\",\n\t],\n\tasync parse(link, _host, path) {\n\t\tconst [first, second, third] = path;\n\t\tif (!first || third) return null;\n\n\t\tif (second && !alphabeticRegex.test(second)) return null;\n\t\telse if (!second && !alphabeticRegex.test(first)) return null;\n\n\t\tconst html = (await request({\n\t\t\turl: link,\n\t\t})).text;\n\n\t\tconst sections = parseNextData<Next>(html)?.props?.pageProps?.pageData?.sections;\n\t\tif (!sections) return null;\n\n\t\tconst links = sections.flatMap(x => x.links ?? []).filter(x => x.url && x.platform);\n\n\t\tconst valid = links.find(x => x.platform === \"spotify\")\n\t\t\t?? links.find(x => x.platform === \"soundcloud\")\n\t\t\t?? links.find(x => x.platform === \"appleMusic\");\n\t\tif (!valid) return null;\n\n\t\treturn await parseLink(valid.url);\n\t},\n};\n","const handlerCache = new Map<string, unknown>();\n\nexport function makeCache<T, Args>(name: string, retrieve: (...args: Args[]) => T) {\n\treturn {\n\t\tretrieve(...args: Args[]) {\n\t\t\tif (handlerCache.has(name)) return handlerCache.get(name) as T;\n\n\t\t\tconst res = retrieve(...args);\n\t\t\tif (res instanceof Promise) {\n\t\t\t\treturn res.then((ret: T) => {\n\t\t\t\t\thandlerCache.set(name, ret);\n\t\t\t\t\treturn ret;\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\thandlerCache.set(name, res);\n\t\t\t\treturn res;\n\t\t\t}\n\t\t},\n\t};\n}\n","import { PLAYLIST_LIMIT, request } from \"handlers/common\";\nimport type { RenderInfoBase, SongService } from \"handlers/helpers\";\n\nimport { makeCache } from \"../cache\";\n\ninterface APIDataEntry {\n\tattributes: {\n\t\turl: string;\n\t\tname: string;\n\t\tartistName?: string;\n\t\tcontentRating?: \"explicit\";\n\t\tdurationInMillis?: number;\n\t\tpreviews?: {\n\t\t\turl: string;\n\t\t}[];\n\t\tartwork?: {\n\t\t\turl: string;\n\t\t};\n\t};\n\trelationships: {\n\t\tsongs?: {\n\t\t\tdata: APIDataEntry[];\n\t\t};\n\t\ttracks?: {\n\t\t\tdata: APIDataEntry[];\n\t\t};\n\t};\n}\n\ninterface APIData {\n\tdata: [APIDataEntry];\n}\n\nconst geo = \"us\", defaultName = \"songspotlight\";\n\nfunction applemusicLink(type: string, id: string) {\n\treturn `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`;\n}\n\nconst applemusicToken = makeCache(\"applemusicToken\", async (html?: string) => {\n\thtml ??= (await request({\n\t\turl: `https://music.apple.com/${geo}/new`,\n\t})).text;\n\n\tconst asset = html.match(/src=\"(\\/assets\\/index~\\w+\\.js)\"/i)?.[1];\n\tif (!asset) return;\n\n\tconst js = (await request({\n\t\turl: `https://music.apple.com${asset}`,\n\t})).text;\n\n\tconst code = js.match(/\\w+=\"(ey.*?)\"/i)?.[1];\n\treturn code;\n});\n\nexport const applemusic: SongService = {\n\tname: \"applemusic\",\n\tlabel: \"Apple Music\",\n\thosts: [\n\t\t\"music.apple.com\",\n\t\t\"geo.music.apple.com\",\n\t],\n\ttypes: [\"artist\", \"album\", \"playlist\", \"song\"],\n\tasync parse(_link, _host, path) {\n\t\tconst [country, type, name, id, fourth] = path;\n\t\tif (!country || !type || !this.types.includes(type) || !name || !id || fourth) return null;\n\n\t\tconst res = await request({\n\t\t\turl: applemusicLink(type, id),\n\t\t});\n\t\tif (res.status !== 200) return null;\n\n\t\tawait applemusicToken.retrieve(res.text);\n\n\t\treturn {\n\t\t\tservice: this.name,\n\t\t\ttype,\n\t\t\tid,\n\t\t};\n\t},\n\tasync render(type, id) {\n\t\tconst token = await applemusicToken.retrieve();\n\t\tif (!token) return null;\n\n\t\tconst res = await request({\n\t\t\turl: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s`,\n\t\t\tquery: {\n\t\t\t\tinclude: \"songs\",\n\t\t\t\tids: id,\n\t\t\t},\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${token}`,\n\t\t\t\torigin: \"https://music.apple.com\",\n\t\t\t},\n\t\t});\n\t\tif (res.status !== 200) return null;\n\n\t\tconst { attributes, relationships } = (res.json as APIData).data[0];\n\n\t\tconst base: RenderInfoBase = {\n\t\t\tlabel: attributes.name,\n\t\t\tsublabel: attributes.artistName ?? \"Top songs\",\n\t\t\tlink: attributes.url,\n\t\t\texplicit: attributes.contentRating === \"explicit\",\n\t\t};\n\t\tconst thumbnailUrl = attributes.artwork?.url?.replace(/{[wh]}/g, \"128\");\n\n\t\tif (type === \"song\") {\n\t\t\tconst duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;\n\t\t\treturn {\n\t\t\t\tform: \"single\",\n\t\t\t\t...base,\n\t\t\t\tthumbnailUrl,\n\t\t\t\tsingle: {\n\t\t\t\t\taudio: previewUrl && duration\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\tpreviewUrl,\n\t\t\t\t\t\t\tduration,\n\t\t\t\t\t\t}\n\t\t\t\t\t\t: undefined,\n\t\t\t\t},\n\t\t\t};\n\t\t} else {\n\t\t\treturn {\n\t\t\t\tform: \"list\",\n\t\t\t\t...base,\n\t\t\t\tthumbnailUrl,\n\t\t\t\tlist: (relationships.tracks?.data ?? relationships.songs?.data ?? []).slice(\n\t\t\t\t\t0,\n\t\t\t\t\tPLAYLIST_LIMIT,\n\t\t\t\t).map(\n\t\t\t\t\t({ attributes }) => {\n\t\t\t\t\t\tconst duration = attributes.durationInMillis,\n\t\t\t\t\t\t\tpreviewUrl = attributes.previews?.[0]?.url;\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tlabel: attributes.name,\n\t\t\t\t\t\t\tsublabel: attributes.artistName!,\n\t\t\t\t\t\t\tlink: attributes.url,\n\t\t\t\t\t\t\texplicit: attributes.contentRating === \"explicit\",\n\t\t\t\t\t\t\taudio: previewUrl && duration\n\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tpreviewUrl,\n\t\t\t\t\t\t\t\t\tduration,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\t},\n\tasync validate(type, id) {\n\t\treturn (await request({\n\t\t\turl: applemusicLink(type, id),\n\t\t})).status === 200;\n\t},\n};\n","import { PLAYLIST_LIMIT, request } from \"handlers/common\";\nimport { parseLink } from \"handlers/finders\";\nimport { type RenderInfoBase, type SongService } from \"handlers/helpers\";\n\ninterface oEmbedData {\n\thtml: string;\n}\n\ninterface Transcoding {\n\tduration: number;\n\turl: string;\n\tformat: {\n\t\tprotocol: string;\n\t};\n}\n\ninterface WidgetData {\n\tartwork_url: string;\n\tavatar_url?: string;\n\ttitle: string;\n\tid: number;\n\tpermalink_url: string;\n\tusername?: string;\n\tuser?: {\n\t\tusername: string;\n\t};\n\tpublisher_metadata?: {\n\t\texplicit: boolean;\n\t};\n\tmedia?: {\n\t\ttranscodings: Transcoding[];\n\t};\n\ttracks?: WidgetData[];\n}\n\ninterface TracksWidgetData {\n\tcollection: WidgetData[];\n}\n\ninterface PreviewResponse {\n\turl: string;\n}\n\nconst client_id = \"nIjtjiYnjkOhMyh5xrbqEW12DxeJVnic\";\n\nfunction parseWidget(type: string, id: string, tracks: true): Promise<TracksWidgetData | undefined>;\nfunction parseWidget(type: string, id: string, tracks: false): Promise<WidgetData | undefined>;\nasync function parseWidget(type: string, id: string, tracks: boolean) {\n\treturn (await request({\n\t\turl: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? \"/tracks\" : \"\"}`,\n\t\tquery: {\n\t\t\tformat: \"json\",\n\t\t\tclient_id,\n\t\t\t// app version isnt static but lets hope soundcloud doesnt mind :) :) :)\n\t\t\tapp_version: \"1768986291\",\n\t\t\tlimit: \"20\",\n\t\t},\n\t})).json;\n}\nasync function parsePreview(transcodings: Transcoding[]) {\n\tconst preview = transcodings.sort((a, b) => {\n\t\tconst isA = a.format.protocol === \"progressive\";\n\t\tconst isB = b.format.protocol === \"progressive\";\n\n\t\treturn (isA && !isB) ? -1 : (isB && !isA) ? 1 : 0;\n\t})?.[0];\n\n\tif (preview?.url && preview?.duration) {\n\t\tconst link = (await request({\n\t\t\turl: preview.url,\n\t\t\tquery: {\n\t\t\t\tclient_id,\n\t\t\t},\n\t\t}))\n\t\t\t.json as PreviewResponse;\n\t\tif (!link?.url) return;\n\n\t\treturn {\n\t\t\tduration: preview.duration,\n\t\t\tpreviewUrl: link.url,\n\t\t};\n\t}\n}\n\nexport const soundcloud: SongService = {\n\tname: \"soundcloud\",\n\tlabel: \"Soundcloud\",\n\thosts: [\n\t\t\"soundcloud.com\",\n\t\t\"m.soundcloud.com\",\n\t\t\"on.soundcloud.com\",\n\t],\n\ttypes: [\"user\", \"track\", \"playlist\"],\n\tasync parse(link, host, path) {\n\t\tif (host === \"on.soundcloud.com\") {\n\t\t\tif (!path[0] || path[1]) return null;\n\t\t\tconst { url, status } = await request({\n\t\t\t\turl: link,\n\t\t\t});\n\t\t\treturn status === 200 ? await parseLink(url) : null;\n\t\t} else {\n\t\t\tconst [user, second, track, fourth] = path;\n\n\t\t\tlet valid = false;\n\t\t\tif (user && !second) valid = true; // user\n\t\t\telse if (user && second && second !== \"sets\" && !track) valid = true; // playlist\n\t\t\telse if (user && second === \"sets\" && track && !fourth) valid = true; // track\n\n\t\t\tif (!valid) return null;\n\n\t\t\tconst data = (await request({\n\t\t\t\turl: \"https://soundcloud.com/oembed\",\n\t\t\t\tquery: {\n\t\t\t\t\tformat: \"json\",\n\t\t\t\t\turl: link,\n\t\t\t\t},\n\t\t\t})).json as oEmbedData;\n\t\t\tif (!data?.html) return null;\n\n\t\t\t// https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F1053322828&show_artwork=true\n\t\t\tconst rawUrl = data.html.match(/w\\.soundcloud\\.com.*?url=(.*?)[&\"]/)?.[1];\n\t\t\tif (!rawUrl) return null;\n\n\t\t\t// https://api.soundcloud.com/tracks/1053322828\n\t\t\tconst splits = decodeURIComponent(rawUrl).split(/\\/+/);\n\t\t\tconst kind = splits[2], id = splits[3];\n\t\t\tif (!kind || !id) return null;\n\n\t\t\treturn {\n\t\t\t\tservice: this.name,\n\t\t\t\ttype: kind.slice(0, -1), // turns tracks -> track\n\t\t\t\tid,\n\t\t\t};\n\t\t}\n\t},\n\tasync render(type, id) {\n\t\tconst data = await parseWidget(type, id, false);\n\t\tif (!data?.id) return null;\n\n\t\tconst base: RenderInfoBase = {\n\t\t\tlabel: data.title ?? data.username,\n\t\t\tsublabel: data.user?.username ?? \"Top tracks\",\n\t\t\tlink: data.permalink_url,\n\t\t\texplicit: Boolean(data.publisher_metadata?.explicit),\n\t\t};\n\t\tconst thumbnailUrl = data.artwork_url ?? data.avatar_url;\n\n\t\tif (type === \"track\") {\n\t\t\tconst audio = await parsePreview(data.media?.transcodings ?? []).catch(() => undefined);\n\n\t\t\treturn {\n\t\t\t\tform: \"single\",\n\t\t\t\t...base,\n\t\t\t\tthumbnailUrl,\n\t\t\t\tsingle: {\n\t\t\t\t\taudio,\n\t\t\t\t},\n\t\t\t};\n\t\t} else {\n\t\t\tlet tracks: WidgetData[] = [];\n\t\t\tif (type === \"user\") {\n\t\t\t\tconst got = await parseWidget(type, id, true).catch(() => undefined);\n\t\t\t\tif (got?.collection) tracks = got.collection;\n\t\t\t} else {\n\t\t\t\tif (data.tracks) tracks = data.tracks;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tform: \"list\",\n\t\t\t\t...base,\n\t\t\t\tthumbnailUrl,\n\t\t\t\tlist: await Promise.all(\n\t\t\t\t\ttracks.filter(x => x.title).slice(0, PLAYLIST_LIMIT).map(async (track) => ({\n\t\t\t\t\t\tlabel: track.title,\n\t\t\t\t\t\tsublabel: track.user?.username ?? \"unknown\",\n\t\t\t\t\t\tlink: track.permalink_url,\n\t\t\t\t\t\texplicit: Boolean(track.publisher_metadata!.explicit),\n\t\t\t\t\t\taudio: await parsePreview(track.media?.transcodings ?? []).catch(() => undefined),\n\t\t\t\t\t})),\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\t},\n\tasync validate(type, id) {\n\t\treturn (await parseWidget(type, id, false))?.id !== undefined;\n\t},\n};\n","import { parseNextData, PLAYLIST_LIMIT, request } from \"handlers/common\";\nimport { type RenderInfoBase, type SongService } from \"handlers/helpers\";\n\ninterface Next {\n\tprops: {\n\t\tpageProps: {\n\t\t\ttitle?: string;\n\t\t\tstate?: {\n\t\t\t\tdata: {\n\t\t\t\t\tentity: {\n\t\t\t\t\t\turi: string;\n\t\t\t\t\t\ttitle: string;\n\t\t\t\t\t\tsubtitle: string;\n\t\t\t\t\t\tisExplicit: boolean;\n\t\t\t\t\t\tartists?: {\n\t\t\t\t\t\t\tname: string;\n\t\t\t\t\t\t}[];\n\t\t\t\t\t\tduration?: number;\n\t\t\t\t\t\taudioPreview?: {\n\t\t\t\t\t\t\turl: string;\n\t\t\t\t\t\t};\n\t\t\t\t\t\ttrackList?: {\n\t\t\t\t\t\t\turi: string;\n\t\t\t\t\t\t\ttitle: string;\n\t\t\t\t\t\t\tsubtitle: string;\n\t\t\t\t\t\t\tisExplicit: boolean;\n\t\t\t\t\t\t\tartists?: {\n\t\t\t\t\t\t\t\tname: string;\n\t\t\t\t\t\t\t}[];\n\t\t\t\t\t\t\tduration?: number;\n\t\t\t\t\t\t\taudioPreview?: {\n\t\t\t\t\t\t\t\turl: string;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}[];\n\t\t\t\t\t\tvisualIdentity: {\n\t\t\t\t\t\t\timage: {\n\t\t\t\t\t\t\t\turl: string;\n\t\t\t\t\t\t\t\tmaxWidth: number;\n\t\t\t\t\t\t\t}[];\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t};\n\t};\n}\n\nasync function parseEmbed(type: string, id: string) {\n\treturn parseNextData<Next>(\n\t\t(await request({\n\t\t\turl: `https://open.spotify.com/embed/${type}/${id}`,\n\t\t})).text,\n\t);\n}\n\nfunction fromUri(uri: string) {\n\tconst [sanityCheck, type, id] = uri.split(\":\");\n\tif (sanityCheck === \"spotify\" && type && id) return `https://open.spotify.com/${type}/${id}`;\n\telse return null;\n}\n\nexport const spotify: SongService = {\n\tname: \"spotify\",\n\tlabel: \"Spotify\",\n\thosts: [\n\t\t\"open.spotify.com\",\n\t],\n\ttypes: [\"track\", \"album\", \"playlist\", \"artist\"],\n\tasync parse(_link, _host, path) {\n\t\tconst [type, id, third] = path;\n\t\tif (!type || !this.types.includes(type as never) || !id || third) return null;\n\n\t\tif (!await this.validate(type, id)) return null;\n\n\t\treturn {\n\t\t\tservice: this.name,\n\t\t\ttype,\n\t\t\tid,\n\t\t};\n\t},\n\tasync render(type, id) {\n\t\tconst data = (await parseEmbed(type, id) as Next)?.props?.pageProps?.state?.data?.entity;\n\t\tif (!data) return null;\n\n\t\tconst base: RenderInfoBase = {\n\t\t\tlabel: data.title,\n\t\t\tsublabel: data.subtitle ?? data.artists?.map(x => x.name).join(\", \"),\n\t\t\tlink: fromUri(data.uri)!,\n\t\t\texplicit: Boolean(data.isExplicit),\n\t\t};\n\t\tconst thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]\n\t\t\t?.url.replace(/:\\/\\/.*?\\.spotifycdn\\.com\\/image/, \"://i.scdn.co/image\");\n\n\t\tif (type === \"track\") {\n\t\t\treturn {\n\t\t\t\tform: \"single\",\n\t\t\t\t...base,\n\t\t\t\tthumbnailUrl,\n\t\t\t\tsingle: {\n\t\t\t\t\taudio: (data.audioPreview && data.duration)\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\tduration: data.duration,\n\t\t\t\t\t\t\tpreviewUrl: data.audioPreview.url,\n\t\t\t\t\t\t}\n\t\t\t\t\t\t: undefined,\n\t\t\t\t},\n\t\t\t};\n\t\t} else {\n\t\t\treturn {\n\t\t\t\tform: \"list\",\n\t\t\t\t...base,\n\t\t\t\tthumbnailUrl,\n\t\t\t\tlist: (data.trackList ?? []).slice(0, PLAYLIST_LIMIT).map((track) => ({\n\t\t\t\t\tlabel: track.title,\n\t\t\t\t\tsublabel: track.subtitle ?? track.artists?.map(x => x.name).join(\", \"),\n\t\t\t\t\tlink: fromUri(track.uri)!,\n\t\t\t\t\texplicit: Boolean(track.isExplicit),\n\t\t\t\t\taudio: (track.audioPreview && track.duration)\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\tduration: track.duration,\n\t\t\t\t\t\t\tpreviewUrl: track.audioPreview.url,\n\t\t\t\t\t\t}\n\t\t\t\t\t\t: undefined,\n\t\t\t\t})),\n\t\t\t};\n\t\t}\n\t},\n\tasync validate(type, id) {\n\t\treturn !(await parseEmbed(type, id))?.props?.pageProps?.title;\n\t},\n};\n","import { songdotlink } from \"./defs/parsers/songdotlink\";\nimport { applemusic } from \"./defs/services/applemusic\";\nimport { soundcloud } from \"./defs/services/soundcloud\";\nimport { spotify } from \"./defs/services/spotify\";\nimport { $ } from \"./finders\";\nimport type { SongParser, SongService } from \"./helpers\";\n\nexport const services = [\n\tspotify,\n\tsoundcloud,\n\tapplemusic,\n] as SongService[];\n$.services = services;\n\nexport const parsers = [\n\tsongdotlink,\n\t...services,\n] as SongParser[];\n$.parsers = parsers;\n"],"mappings":"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"}
@@ -0,0 +1,150 @@
1
+ import { sid } from "./util.js";
2
+
3
+ //#region package.json
4
+ var version = "2.0.1";
5
+
6
+ //#endregion
7
+ //#region src/handlers/common.ts
8
+ function clean(link) {
9
+ const url = new URL(link);
10
+ url.protocol = "https";
11
+ url.username = url.password = url.port = url.search = url.hash = "";
12
+ return url.toString().replace(/\/?$/, "");
13
+ }
14
+ let makeRequest = fetch;
15
+ /**
16
+ * Lets you to set a custom `fetch()` function. Useful for passing requests through Electron's [net.fetch](https://www.electronjs.org/docs/latest/api/net#netfetchinput-init) for example.
17
+ * @example ```ts
18
+ * import { net } from "electron";
19
+ *
20
+ * setFetchHandler(net.fetch as unknown as typeof fetch);
21
+ * ```
22
+ */
23
+ function setFetchHandler(fetcher) {
24
+ makeRequest = fetcher;
25
+ }
26
+ async function request(options) {
27
+ if (options.body) {
28
+ const body = JSON.stringify(options.body);
29
+ options.body = body;
30
+ options.headers ??= {};
31
+ options.headers["content-type"] ??= "application/json";
32
+ options.headers["content-length"] ??= String(body.length);
33
+ }
34
+ const url = new URL(options.url);
35
+ for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
36
+ const res = await makeRequest(url, {
37
+ method: options.method,
38
+ redirect: "follow",
39
+ headers: {
40
+ "accept": "*/*",
41
+ "user-agent": `SongSpotlight/${version}`,
42
+ "cache-control": "public, max-age=3600",
43
+ ...options.headers ?? {}
44
+ },
45
+ cf: {
46
+ cacheTtl: 3600,
47
+ cacheEverything: true
48
+ },
49
+ body: options.body
50
+ });
51
+ const text = await res.text();
52
+ let json;
53
+ try {
54
+ json = JSON.parse(text);
55
+ } catch {
56
+ json = null;
57
+ }
58
+ return {
59
+ ok: res.ok,
60
+ redirected: res.redirected,
61
+ url: res.url,
62
+ status: res.status,
63
+ headers: res.headers,
64
+ text,
65
+ json
66
+ };
67
+ }
68
+ function parseNextData(html) {
69
+ const data = html.match(/id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/)?.[1];
70
+ if (!data) return void 0;
71
+ try {
72
+ return JSON.parse(data);
73
+ } catch {
74
+ return;
75
+ }
76
+ }
77
+ const PLAYLIST_LIMIT = 15;
78
+
79
+ //#endregion
80
+ //#region src/handlers/finders.ts
81
+ const $ = {
82
+ services: [],
83
+ parsers: []
84
+ };
85
+ const parseCache = /* @__PURE__ */ new Map();
86
+ const validateCache = /* @__PURE__ */ new Map();
87
+ /**
88
+ * Tries to parse the provided **link**. Returns a **Song** if successful, or `null` if nothing was found. Either response is temporarily cached.
89
+ * @example ```ts
90
+ * await parseLink("https://soundcloud.com/c0ncernn");
91
+ * // { service: "soundcloud", type: "user", id: "914653456" }
92
+ * ```
93
+ */
94
+ async function parseLink(link) {
95
+ const cleaned = clean(link);
96
+ if (parseCache.has(cleaned)) return parseCache.get(cleaned);
97
+ const { hostname, pathname } = new URL(cleaned);
98
+ const path = pathname.slice(1).split(/\/+/);
99
+ let song = null;
100
+ for (const parser of $.parsers) if (parser.hosts.includes(hostname)) {
101
+ song = await parser.parse(cleaned, hostname, path);
102
+ if (song) break;
103
+ }
104
+ parseCache.set(cleaned, song);
105
+ if (song) validateCache.set(sid(song), true);
106
+ return song;
107
+ }
108
+ const renderCache = /* @__PURE__ */ new Map();
109
+ /**
110
+ * Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is temporarily cached.
111
+ * @example ```ts
112
+ * await renderSong({ service: "soundcloud", type: "user", id: "914653456" });
113
+ * // { label: "leroy", sublabel: "Top tracks", explicit: false, form: "list", ... }
114
+ * ```
115
+ */
116
+ async function renderSong(song) {
117
+ const id = sid(song);
118
+ if (renderCache.has(id)) return renderCache.get(id);
119
+ let info = null;
120
+ const service = $.services.find((x) => x.name === song.service);
121
+ if (service?.types.includes(song.type)) info = await service.render(song.type, song.id);
122
+ renderCache.set(id, info);
123
+ if (song) validateCache.set(sid(song), true);
124
+ return info;
125
+ }
126
+ /**
127
+ * Validates if the provided **Song** exists. Returns a `boolean` depending on if the check was successful or not. Either response is temporarily cached.
128
+ * @example ```ts
129
+ * await renderSong({ service: "soundcloud", type: "user", id: "914653456" });
130
+ * // true
131
+ * ```
132
+ */
133
+ async function validateSong(song) {
134
+ const id = sid(song);
135
+ if (validateCache.has(id)) return validateCache.get(id);
136
+ let valid = false;
137
+ const service = $.services.find((x) => x.name === song.service);
138
+ if (service?.types.includes(song.type)) valid = await service.validate(song.type, song.id);
139
+ validateCache.set(id, valid);
140
+ return valid;
141
+ }
142
+ /** Clears the cache for all handler functions */
143
+ function clearCache() {
144
+ parseCache.clear();
145
+ renderCache.clear();
146
+ validateCache.clear();
147
+ }
148
+
149
+ //#endregion
150
+ export { validateSong as a, request as c, renderSong as i, setFetchHandler as l, clearCache as n, PLAYLIST_LIMIT as o, parseLink as r, parseNextData as s, $ as t };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finders-CDZyaavx.js","names":[],"sources":["../package.json","../src/handlers/common.ts","../src/handlers/finders.ts"],"sourcesContent":["","import { version } from \"@\";\n\ninterface RequestOptions {\n\turl: string;\n\tmethod?: string;\n\tquery?: Record<string, string>;\n\theaders?: Record<string, string>;\n\tbody?: unknown;\n}\n\nexport function clean(link: string) {\n\tconst url = new URL(link);\n\turl.protocol = \"https\";\n\turl.username =\n\t\turl.password =\n\t\turl.port =\n\t\turl.search =\n\t\turl.hash =\n\t\t\t\"\";\n\n\treturn url.toString().replace(/\\/?$/, \"\");\n}\n\nlet makeRequest = fetch;\n/**\n * Lets you to set a custom `fetch()` function. Useful for passing requests through Electron's [net.fetch](https://www.electronjs.org/docs/latest/api/net#netfetchinput-init) for example.\n * @example ```ts\n * import { net } from \"electron\";\n *\n * setFetchHandler(net.fetch as unknown as typeof fetch);\n * ```\n */\nexport function setFetchHandler(fetcher: typeof fetch) {\n\tmakeRequest = fetcher;\n}\n\nexport async function request(options: RequestOptions) {\n\tif (options.body) {\n\t\tconst body = JSON.stringify(options.body);\n\t\toptions.body = body;\n\n\t\toptions.headers ??= {};\n\t\toptions.headers[\"content-type\"] ??= \"application/json\";\n\t\toptions.headers[\"content-length\"] ??= String(body.length);\n\t}\n\n\tconst url = new URL(options.url);\n\tfor (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);\n\n\tconst res = await makeRequest(url, {\n\t\tmethod: options.method,\n\t\tredirect: \"follow\",\n\t\theaders: {\n\t\t\t\"accept\": \"*/*\",\n\t\t\t\"user-agent\": `SongSpotlight/${version}`,\n\t\t\t\"cache-control\": \"public, max-age=3600\",\n\t\t\t...(options.headers ?? {}),\n\t\t},\n\t\t// @ts-expect-error Untyped cloudflare workers cache type\n\t\tcf: {\n\t\t\tcacheTtl: 3600,\n\t\t\tcacheEverything: true,\n\t\t},\n\t\tbody: options.body as never,\n\t});\n\n\tconst text = await res.text();\n\tlet json: unknown;\n\ttry {\n\t\tjson = JSON.parse(text);\n\t} catch {\n\t\tjson = null;\n\t}\n\n\treturn {\n\t\tok: res.ok,\n\t\tredirected: res.redirected,\n\t\turl: res.url,\n\t\tstatus: res.status,\n\t\theaders: res.headers,\n\t\ttext,\n\t\tjson,\n\t};\n}\n\nexport function parseNextData<Type>(html: string) {\n\tconst data = html.match(/id=\"__NEXT_DATA__\" type=\"application\\/json\">(.*?)<\\/script>/)?.[1];\n\tif (!data) return undefined;\n\n\ttry {\n\t\treturn JSON.parse(data) as Type;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n// limit of 15 songs per playlist\nexport const PLAYLIST_LIMIT = 15;\n","import type { Song } from \"structs/types\";\n\nimport { sid } from \"$util\";\n\nimport { clean } from \"./common\";\nimport type { RenderSongInfo, SongParser, SongService } from \"./helpers\";\n\n// introducing... a pointer! resolves the circular dependency\nexport const $ = {\n\tservices: [] as SongService[],\n\tparsers: [] as SongParser[],\n};\n\n// parseLink -> parseCache, validateCache\n// rebuildLink -> linkCache, validateCache\n// renderSong -> renderCache, validateCache\n// validateSong -> validateCache\n\nconst parseCache = new Map<string, Song | null>();\nconst validateCache = new Map<string, boolean>();\n/**\n * Tries to parse the provided **link**. Returns a **Song** if successful, or `null` if nothing was found. Either response is temporarily cached.\n * @example ```ts\n * await parseLink(\"https://soundcloud.com/c0ncernn\");\n * // { service: \"soundcloud\", type: \"user\", id: \"914653456\" }\n * ```\n */\nexport async function parseLink(link: string): Promise<Song | null> {\n\tconst cleaned = clean(link);\n\tif (parseCache.has(cleaned)) return parseCache.get(cleaned)!;\n\n\tconst { hostname, pathname } = new URL(cleaned);\n\tconst path = pathname.slice(1).split(/\\/+/);\n\n\tlet song: Song | null = null;\n\tfor (const parser of $.parsers) {\n\t\tif (parser.hosts.includes(hostname)) {\n\t\t\tsong = await parser.parse(cleaned, hostname, path);\n\t\t\tif (song) break;\n\t\t}\n\t}\n\n\tparseCache.set(cleaned, song);\n\tif (song) validateCache.set(sid(song), true);\n\treturn song;\n}\n\nconst renderCache = new Map<string, RenderSongInfo | null>();\n/**\n * Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is temporarily cached.\n * @example ```ts\n * await renderSong({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\n * // { label: \"leroy\", sublabel: \"Top tracks\", explicit: false, form: \"list\", ... }\n * ```\n */\nexport async function renderSong(song: Song): Promise<RenderSongInfo | null> {\n\tconst id = sid(song);\n\tif (renderCache.has(id)) return renderCache.get(id)!;\n\n\tlet info: RenderSongInfo | null = null;\n\tconst service = $.services.find(x => x.name === song.service);\n\tif (service?.types.includes(song.type)) info = await service.render(song.type, song.id);\n\n\trenderCache.set(id, info);\n\tif (song) validateCache.set(sid(song), true);\n\treturn info;\n}\n\n/**\n * Validates if the provided **Song** exists. Returns a `boolean` depending on if the check was successful or not. Either response is temporarily cached.\n * @example ```ts\n * await renderSong({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\n * // true\n * ```\n */\nexport async function validateSong(song: Song): Promise<boolean> {\n\tconst id = sid(song);\n\tif (validateCache.has(id)) return validateCache.get(id)!;\n\n\tlet valid = false;\n\tconst service = $.services.find(x => x.name === song.service);\n\tif (service?.types.includes(song.type)) valid = await service.validate(song.type, song.id);\n\n\tvalidateCache.set(id, valid);\n\treturn valid;\n}\n\n/** Clears the cache for all handler functions */\nexport function clearCache() {\n\tparseCache.clear();\n\trenderCache.clear();\n\tvalidateCache.clear();\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
- import { Song } from "./types-B2sGtUCQ.js";
1
+ import { t as Song } from "./types-DRQ6d925.js";
2
2
 
3
3
  //#region src/handlers/helpers.d.ts
4
4
  type MaybePromise<T> = Promise<T> | T;
5
5
  interface SongParser {
6
6
  name: string;
7
+ label: string;
7
8
  hosts: string[];
8
9
  parse(link: string, host: string, path: string[]): MaybePromise<Song | null>;
9
10
  }
@@ -11,19 +12,18 @@ interface SongService extends SongParser {
11
12
  types: string[];
12
13
  render(type: string, id: string): MaybePromise<RenderSongInfo | null>;
13
14
  validate(type: string, id: string): MaybePromise<boolean>;
14
- rebuild(type: string, id: string): MaybePromise<string | null>;
15
15
  }
16
16
  interface RenderInfoBase {
17
17
  label: string;
18
18
  sublabel: string;
19
19
  explicit: boolean;
20
+ link: string;
20
21
  }
21
22
  interface RenderInfoEntry {
22
23
  audio?: {
23
24
  duration: number;
24
25
  previewUrl: string;
25
- } | undefined;
26
- link: string;
26
+ };
27
27
  }
28
28
  type RenderInfoEntryBased = RenderInfoEntry & RenderInfoBase;
29
29
  interface RenderSongSingle extends RenderInfoBase {
@@ -55,14 +55,6 @@ declare const $: {
55
55
  * ```
56
56
  */
57
57
  declare function parseLink(link: string): Promise<Song | null>;
58
- /**
59
- * Tries to recreate the link to the provided **Song**. Returns `string` if successful, or `null` if nothing was found. Either response is temporarily cached.
60
- * @example ```ts
61
- * await parseLink({ service: "soundcloud", type: "user", id: "914653456" });
62
- * // https://soundcloud.com/c0ncernn
63
- * ```
64
- */
65
- declare function rebuildLink(song: Song): Promise<string | null>;
66
58
  /**
67
59
  * Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is temporarily cached.
68
60
  * @example ```ts
@@ -82,4 +74,4 @@ declare function validateSong(song: Song): Promise<boolean>;
82
74
  /** Clears the cache for all handler functions */
83
75
  declare function clearCache(): void;
84
76
  //#endregion
85
- export { $, RenderInfoBase, RenderInfoEntry, RenderInfoEntryBased, RenderSongInfo, SongParser, SongService, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
77
+ export { $, RenderInfoBase, RenderInfoEntry, RenderInfoEntryBased, RenderSongInfo, SongParser, SongService, clearCache, parseLink, parsers, renderSong, services, validateSong };
package/dist/handlers.js CHANGED
@@ -1,4 +1,4 @@
1
- import "./common-DrSlxvDY.js";
2
- import { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong } from "./core-XI62QUiR.js";
1
+ import { a as validateSong, i as renderSong, n as clearCache, r as parseLink, t as $ } from "./finders-CDZyaavx.js";
2
+ import { n as services, t as parsers } from "./core-CGspipcB.js";
3
3
 
4
- export { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
4
+ export { $, clearCache, parseLink, parsers, renderSong, services, validateSong };
package/dist/structs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Song, UserData } from "./types-B2sGtUCQ.js";
1
+ import { n as UserData, t as Song } from "./types-DRQ6d925.js";
2
2
  import z, { ZodLiteral, ZodObject, ZodString, core } from "zod";
3
3
 
4
4
  //#region src/structs/zod.d.ts
package/dist/structs.js CHANGED
@@ -1,5 +1,4 @@
1
- import "./common-DrSlxvDY.js";
2
- import { services } from "./core-XI62QUiR.js";
1
+ import { n as services } from "./core-CGspipcB.js";
3
2
  import z from "zod";
4
3
 
5
4
  //#region src/structs/zod.ts
@@ -1 +1 @@
1
- {"version":3,"file":"structs.js","names":[],"sources":["../src/structs/zod.ts"],"sourcesContent":["import { services } from \"handlers/core\";\r\nimport type { core, ZodLiteral, ZodObject, ZodString } from \"zod\";\r\nimport z from \"zod\";\r\n\r\ntype SongDef = ZodObject<\r\n\t{\r\n\t\tservice: ZodLiteral<string>;\r\n\t\ttype: ZodLiteral<string>; // this is a ZodUnion, but casting it as that type lead to some issues with typescript\r\n\t\tid: ZodString;\r\n\t},\r\n\tcore.$strip\r\n>;\r\n\r\nexport const SongSchema = z.discriminatedUnion(\r\n\t\"service\",\r\n\tservices.map((service) =>\r\n\t\tz.object({\r\n\t\t\tservice: z.literal(service.name),\r\n\t\t\ttype: z.union(service.types.map(type => z.literal(type))),\r\n\t\t\tid: z.string(),\r\n\t\t})\r\n\t) as unknown as [SongDef],\r\n);\r\n\r\n/** **UserDataSchema** does not have a limit by default */\r\nexport const UserDataSchema = z.array(SongSchema);\r\n"],"mappings":";;;;;AAaA,MAAa,aAAa,EAAE,mBAC3B,WACA,SAAS,KAAK,YACb,EAAE,OAAO;CACR,SAAS,EAAE,QAAQ,QAAQ;CAC3B,MAAM,EAAE,MAAM,QAAQ,MAAM,KAAI,SAAQ,EAAE,QAAQ;CAClD,IAAI,EAAE;;;AAMT,MAAa,iBAAiB,EAAE,MAAM"}
1
+ {"version":3,"file":"structs.js","names":[],"sources":["../src/structs/zod.ts"],"sourcesContent":["import { services } from \"handlers/core\";\nimport type { core, ZodLiteral, ZodObject, ZodString } from \"zod\";\nimport z from \"zod\";\n\ntype SongDef = ZodObject<\n\t{\n\t\tservice: ZodLiteral<string>;\n\t\ttype: ZodLiteral<string>; // this is a ZodUnion, but casting it as that type lead to some issues with typescript\n\t\tid: ZodString;\n\t},\n\tcore.$strip\n>;\n\nexport const SongSchema = z.discriminatedUnion(\n\t\"service\",\n\tservices.map((service) =>\n\t\tz.object({\n\t\t\tservice: z.literal(service.name),\n\t\t\ttype: z.union(service.types.map(type => z.literal(type))),\n\t\t\tid: z.string(),\n\t\t})\n\t) as unknown as [SongDef],\n);\n\n/** **UserDataSchema** does not have a limit by default */\nexport const UserDataSchema = z.array(SongSchema);\n"],"mappings":";;;;AAaA,MAAa,aAAa,EAAE,mBAC3B,WACA,SAAS,KAAK,YACb,EAAE,OAAO;CACR,SAAS,EAAE,QAAQ,QAAQ,KAAK;CAChC,MAAM,EAAE,MAAM,QAAQ,MAAM,KAAI,SAAQ,EAAE,QAAQ,KAAK,CAAC,CAAC;CACzD,IAAI,EAAE,QAAQ;CACd,CAAC,CACF,CACD;;AAGD,MAAa,iBAAiB,EAAE,MAAM,WAAW"}
@@ -6,4 +6,4 @@ interface Song {
6
6
  }
7
7
  type UserData = Song[];
8
8
  //#endregion
9
- export { Song, UserData };
9
+ export { UserData as n, Song as t };
package/dist/util.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- //#region src/handlers/common.d.ts
1
+ import { t as Song } from "./types-DRQ6d925.js";
2
+ import { RenderSongInfo } from "@song-spotlight/api/handlers";
2
3
 
4
+ //#region src/handlers/common.d.ts
3
5
  /**
4
6
  * Lets you to set a custom `fetch()` function. Useful for passing requests through Electron's [net.fetch](https://www.electronjs.org/docs/latest/api/net#netfetchinput-init) for example.
5
7
  * @example ```ts
@@ -10,4 +12,30 @@
10
12
  */
11
13
  declare function setFetchHandler(fetcher: typeof fetch): void;
12
14
  //#endregion
13
- export { setFetchHandler };
15
+ //#region src/util.d.ts
16
+ /**
17
+ * Returns whether the specified **Song** should have a tall layout (for **playlists**, **albums** and **artists**) or a short layout (for **tracks**).
18
+ * @example ```ts
19
+ * isListLayout({ service: "soundcloud", type: "user", id: "914653456" });
20
+ * // true
21
+ * ```
22
+ */
23
+ declare function isListLayout(song: Song, render?: RenderSongInfo): boolean;
24
+ /**
25
+ * Loops through all **services** and returns the corresponding **service**'s label.
26
+ * @example ```ts
27
+ * getServiceLabel("applemusic");
28
+ * // "Apple Music"
29
+ * ```
30
+ */
31
+ declare function getServiceLabel(service: string): string | undefined;
32
+ /**
33
+ * Helper function which stringifies a **Song**, useful for caching or for using as keys.
34
+ * @example ```ts
35
+ * sid({ service: "soundcloud", type: "user", id: "914653456" });
36
+ * // "soundcloud:user:914653456"
37
+ * ```
38
+ */
39
+ declare function sid(song: Song): string;
40
+ //#endregion
41
+ export { getServiceLabel, isListLayout, setFetchHandler, sid };
package/dist/util.js CHANGED
@@ -1,3 +1,40 @@
1
- import { setFetchHandler } from "./common-DrSlxvDY.js";
1
+ import { l as setFetchHandler, t as $ } from "./finders-CDZyaavx.js";
2
2
 
3
- export { setFetchHandler };
3
+ //#region src/util.ts
4
+ /**
5
+ * Returns whether the specified **Song** should have a tall layout (for **playlists**, **albums** and **artists**) or a short layout (for **tracks**).
6
+ * @example ```ts
7
+ * isListLayout({ service: "soundcloud", type: "user", id: "914653456" });
8
+ * // true
9
+ * ```
10
+ */
11
+ function isListLayout(song, render) {
12
+ return render?.form === "list" || !["track", "song"].includes(song.type);
13
+ }
14
+ /**
15
+ * Loops through all **services** and returns the corresponding **service**'s label.
16
+ * @example ```ts
17
+ * getServiceLabel("applemusic");
18
+ * // "Apple Music"
19
+ * ```
20
+ */
21
+ function getServiceLabel(service) {
22
+ for (const serviced of $.services) if (serviced.name === service) return serviced.label;
23
+ }
24
+ /**
25
+ * Helper function which stringifies a **Song**, useful for caching or for using as keys.
26
+ * @example ```ts
27
+ * sid({ service: "soundcloud", type: "user", id: "914653456" });
28
+ * // "soundcloud:user:914653456"
29
+ * ```
30
+ */
31
+ function sid(song) {
32
+ return [
33
+ song.service,
34
+ song.type,
35
+ song.id
36
+ ].join(":");
37
+ }
38
+
39
+ //#endregion
40
+ export { getServiceLabel, isListLayout, setFetchHandler, sid };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","names":[],"sources":["../src/util.ts"],"sourcesContent":["import type { RenderSongInfo } from \"@song-spotlight/api/handlers\";\nimport { $ } from \"handlers/finders\";\nimport type { Song } from \"structs/types\";\n\nexport { setFetchHandler } from \"./handlers/common\";\n\n/**\n * Returns whether the specified **Song** should have a tall layout (for **playlists**, **albums** and **artists**) or a short layout (for **tracks**).\n * @example ```ts\n * isListLayout({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\n * // true\n * ```\n */\nexport function isListLayout(song: Song, render?: RenderSongInfo) {\n\treturn render?.form === \"list\" || ![\"track\", \"song\"].includes(song.type);\n}\n\n/**\n * Loops through all **services** and returns the corresponding **service**'s label.\n * @example ```ts\n * getServiceLabel(\"applemusic\");\n * // \"Apple Music\"\n * ```\n */\nexport function getServiceLabel(service: string) {\n\tfor (const serviced of $.services) {\n\t\tif (serviced.name === service) return serviced.label;\n\t}\n}\n\n/**\n * Helper function which stringifies a **Song**, useful for caching or for using as keys.\n * @example ```ts\n * sid({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\n * // \"soundcloud:user:914653456\"\n * ```\n */\nexport function sid(song: Song) {\n\treturn [song.service, song.type, song.id].join(\":\");\n}\n"],"mappings":";;;;;;;;;;AAaA,SAAgB,aAAa,MAAY,QAAyB;AACjE,QAAO,QAAQ,SAAS,UAAU,CAAC,CAAC,SAAS,OAAO,CAAC,SAAS,KAAK,KAAK;;;;;;;;;AAUzE,SAAgB,gBAAgB,SAAiB;AAChD,MAAK,MAAM,YAAY,EAAE,SACxB,KAAI,SAAS,SAAS,QAAS,QAAO,SAAS;;;;;;;;;AAWjD,SAAgB,IAAI,MAAY;AAC/B,QAAO;EAAC,KAAK;EAAS,KAAK;EAAM,KAAK;EAAG,CAAC,KAAK,IAAI"}
package/package.json CHANGED
@@ -1,51 +1,53 @@
1
1
  {
2
- "name": "@song-spotlight/api",
3
- "version": "1.3.2",
4
- "description": "Song Spotlight API types and song validation module",
5
- "type": "module",
6
- "scripts": {
7
- "build": "rm -rf dist && rolldown -c rolldown.config.mts",
8
- "prepublishOnly": "bun run build"
9
- },
10
- "exports": {
11
- "./handlers": {
12
- "types": "./dist/handlers.d.ts",
13
- "default": "./dist/handlers.js"
14
- },
15
- "./structs": {
16
- "types": "./dist/structs.d.ts",
17
- "default": "./dist/structs.js"
18
- },
19
- "./util": {
20
- "types": "./dist/util.d.ts",
21
- "default": "./dist/util.js"
22
- }
23
- },
24
- "publishConfig": {
25
- "access": "public"
26
- },
27
- "files": [
28
- "dist"
29
- ],
30
- "license": "MIT",
31
- "homepage": "https://github.com/nexpid-labs/SongSpotlight/tree/main/packages/api",
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/nexpid-labs/SongSpotlight.git",
35
- "directory": "packages/api"
36
- },
37
- "bugs": {
38
- "url": "https://github.com/nexpid-labs/SongSpotlight/issues"
39
- },
40
- "devDependencies": {
41
- "@types/bun": "latest",
42
- "rolldown": "^1.0.0-beta.34",
43
- "rolldown-plugin-dts": "^0.15.10"
44
- },
45
- "peerDependencies": {
46
- "typescript": "^5"
47
- },
48
- "optionalDependencies": {
49
- "zod": "^4.3.6"
50
- }
51
- }
2
+ "name": "@song-spotlight/api",
3
+ "version": "2.0.1",
4
+ "description": "Song Spotlight API types and song validation module",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "rm -rf dist && rolldown -c rolldown.config.mjs",
8
+ "format": "dprint fmt",
9
+ "typecheck": "tsc --noEmit",
10
+ "prepublishOnly": "bun run build"
11
+ },
12
+ "exports": {
13
+ "./handlers": {
14
+ "types": "./dist/handlers.d.ts",
15
+ "default": "./dist/handlers.js"
16
+ },
17
+ "./structs": {
18
+ "types": "./dist/structs.d.ts",
19
+ "default": "./dist/structs.js"
20
+ },
21
+ "./util": {
22
+ "types": "./dist/util.d.ts",
23
+ "default": "./dist/util.js"
24
+ }
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "license": "MIT",
33
+ "homepage": "https://github.com/nexpid-labs/SongSpotlight/tree/main/packages/api",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/nexpid-labs/SongSpotlight.git",
37
+ "directory": "packages/api"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/nexpid-labs/SongSpotlight/issues"
41
+ },
42
+ "devDependencies": {
43
+ "@types/bun": "^1.3.8",
44
+ "rolldown": "^1.0.0-rc.3",
45
+ "rolldown-plugin-dts": "^0.21.9"
46
+ },
47
+ "peerDependencies": {
48
+ "typescript": "^5"
49
+ },
50
+ "optionalDependencies": {
51
+ "zod": "^4.3.6"
52
+ }
53
+ }
@@ -1,78 +0,0 @@
1
- //#region package.json
2
- var version = "1.3.2";
3
-
4
- //#endregion
5
- //#region src/handlers/common.ts
6
- function clean(link) {
7
- const url = new URL(link);
8
- url.protocol = "https";
9
- url.username = url.password = url.port = url.search = url.hash = "";
10
- return url.toString().replace(/\/?$/, "");
11
- }
12
- let makeRequest = fetch;
13
- /**
14
- * Lets you to set a custom `fetch()` function. Useful for passing requests through Electron's [net.fetch](https://www.electronjs.org/docs/latest/api/net#netfetchinput-init) for example.
15
- * @example ```ts
16
- * import { net } from "electron";
17
- *
18
- * setFetchHandler(net.fetch as unknown as typeof fetch);
19
- * ```
20
- */
21
- function setFetchHandler(fetcher) {
22
- makeRequest = fetcher;
23
- }
24
- async function request(options) {
25
- if (options.body) {
26
- const body = JSON.stringify(options.body);
27
- options.body = body;
28
- options.headers ??= {};
29
- options.headers["content-type"] ??= "application/json";
30
- options.headers["content-length"] ??= String(body.length);
31
- }
32
- const url = new URL(options.url);
33
- for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
34
- const res = await makeRequest(url, {
35
- method: options.method,
36
- redirect: "follow",
37
- headers: {
38
- "accept": "*/*",
39
- "user-agent": `SongSpotlight/${version}`,
40
- "cache-control": "public, max-age=3600",
41
- ...options.headers ?? {}
42
- },
43
- cf: {
44
- cacheTtl: 3600,
45
- cacheEverything: true
46
- },
47
- body: options.body
48
- });
49
- const text = await res.text();
50
- let json;
51
- try {
52
- json = JSON.parse(text);
53
- } catch {
54
- json = null;
55
- }
56
- return {
57
- ok: res.ok,
58
- redirected: res.redirected,
59
- url: res.url,
60
- status: res.status,
61
- headers: res.headers,
62
- text,
63
- json
64
- };
65
- }
66
- function parseNextData(html) {
67
- const data = html.match(/id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/)?.[1];
68
- if (!data) return void 0;
69
- try {
70
- return JSON.parse(data);
71
- } catch {
72
- return void 0;
73
- }
74
- }
75
- const PLAYLIST_LIMIT = 15;
76
-
77
- //#endregion
78
- export { PLAYLIST_LIMIT, clean, parseNextData, request, setFetchHandler };
@@ -1 +0,0 @@
1
- {"version":3,"file":"common-DrSlxvDY.js","names":["json: unknown"],"sources":["../package.json","../src/handlers/common.ts"],"sourcesContent":["{\n \"name\": \"@song-spotlight/api\",\n \"version\": \"1.3.2\",\n \"description\": \"Song Spotlight API types and song validation module\",\n \"type\": \"module\",\n \"scripts\": {\n \"build\": \"rm -rf dist && rolldown -c rolldown.config.mts\",\n \"prepublishOnly\": \"bun run build\"\n },\n \"exports\": {\n \"./handlers\": {\n \"types\": \"./dist/handlers.d.ts\",\n \"default\": \"./dist/handlers.js\"\n },\n \"./structs\": {\n \"types\": \"./dist/structs.d.ts\",\n \"default\": \"./dist/structs.js\"\n },\n \"./util\": {\n \"types\": \"./dist/util.d.ts\",\n \"default\": \"./dist/util.js\"\n }\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"files\": [\n \"dist\"\n ],\n \"license\": \"MIT\",\n \"homepage\": \"https://github.com/nexpid-labs/SongSpotlight/tree/main/packages/api\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/nexpid-labs/SongSpotlight.git\",\n \"directory\": \"packages/api\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/nexpid-labs/SongSpotlight/issues\"\n },\n \"devDependencies\": {\n \"@types/bun\": \"latest\",\n \"rolldown\": \"^1.0.0-beta.34\",\n \"rolldown-plugin-dts\": \"^0.15.10\"\n },\n \"peerDependencies\": {\n \"typescript\": \"^5\"\n },\n \"optionalDependencies\": {\n \"zod\": \"^4.3.6\"\n }\n}","import { version } from \"@\";\r\n\r\ninterface RequestOptions {\r\n\turl: string;\r\n\tmethod?: string;\r\n\tquery?: Record<string, string>;\r\n\theaders?: Record<string, string>;\r\n\tbody?: unknown;\r\n}\r\n\r\nexport function clean(link: string) {\r\n\tconst url = new URL(link);\r\n\turl.protocol = \"https\";\r\n\turl.username =\r\n\t\turl.password =\r\n\t\turl.port =\r\n\t\turl.search =\r\n\t\turl.hash =\r\n\t\t\t\"\";\r\n\r\n\treturn url.toString().replace(/\\/?$/, \"\");\r\n}\r\n\r\nlet makeRequest = fetch;\r\n/**\r\n * Lets you to set a custom `fetch()` function. Useful for passing requests through Electron's [net.fetch](https://www.electronjs.org/docs/latest/api/net#netfetchinput-init) for example.\r\n * @example ```ts\r\n * import { net } from \"electron\";\r\n *\r\n * setFetchHandler(net.fetch as unknown as typeof fetch);\r\n * ```\r\n */\r\nexport function setFetchHandler(fetcher: typeof fetch) {\r\n\tmakeRequest = fetcher;\r\n}\r\n\r\nexport async function request(options: RequestOptions) {\r\n\tif (options.body) {\r\n\t\tconst body = JSON.stringify(options.body);\r\n\t\toptions.body = body;\r\n\r\n\t\toptions.headers ??= {};\r\n\t\toptions.headers[\"content-type\"] ??= \"application/json\";\r\n\t\toptions.headers[\"content-length\"] ??= String(body.length);\r\n\t}\r\n\r\n\tconst url = new URL(options.url);\r\n\tfor (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);\r\n\r\n\tconst res = await makeRequest(url, {\r\n\t\tmethod: options.method,\r\n\t\tredirect: \"follow\",\r\n\t\theaders: {\r\n\t\t\t\"accept\": \"*/*\",\r\n\t\t\t\"user-agent\": `SongSpotlight/${version}`,\r\n\t\t\t\"cache-control\": \"public, max-age=3600\",\r\n\t\t\t...(options.headers ?? {}),\r\n\t\t},\r\n\t\t// @ts-expect-error Untyped cloudflare workers cache type\r\n\t\tcf: {\r\n\t\t\tcacheTtl: 3600,\r\n\t\t\tcacheEverything: true,\r\n\t\t},\r\n\t\tbody: options.body as never,\r\n\t});\r\n\r\n\tconst text = await res.text();\r\n\tlet json: unknown;\r\n\ttry {\r\n\t\tjson = JSON.parse(text);\r\n\t} catch {\r\n\t\tjson = null;\r\n\t}\r\n\r\n\treturn {\r\n\t\tok: res.ok,\r\n\t\tredirected: res.redirected,\r\n\t\turl: res.url,\r\n\t\tstatus: res.status,\r\n\t\theaders: res.headers,\r\n\t\ttext,\r\n\t\tjson,\r\n\t};\r\n}\r\n\r\nexport function parseNextData<Type>(html: string) {\r\n\tconst data = html.match(/id=\"__NEXT_DATA__\" type=\"application\\/json\">(.*?)<\\/script>/)?.[1];\r\n\tif (!data) return undefined;\r\n\r\n\ttry {\r\n\t\treturn JSON.parse(data) as Type;\r\n\t} catch {\r\n\t\treturn undefined;\r\n\t}\r\n}\r\n\r\n// limit of 15 songs per playlist\r\nexport const PLAYLIST_LIMIT = 15;\r\n"],"mappings":";cAEe;;;;ACQf,SAAgB,MAAM,MAAc;CACnC,MAAM,MAAM,IAAI,IAAI;AACpB,KAAI,WAAW;AACf,KAAI,WACH,IAAI,WACJ,IAAI,OACJ,IAAI,SACJ,IAAI,OACH;AAEF,QAAO,IAAI,WAAW,QAAQ,QAAQ;;AAGvC,IAAI,cAAc;;;;;;;;;AASlB,SAAgB,gBAAgB,SAAuB;AACtD,eAAc;;AAGf,eAAsB,QAAQ,SAAyB;AACtD,KAAI,QAAQ,MAAM;EACjB,MAAM,OAAO,KAAK,UAAU,QAAQ;AACpC,UAAQ,OAAO;AAEf,UAAQ,YAAY;AACpB,UAAQ,QAAQ,oBAAoB;AACpC,UAAQ,QAAQ,sBAAsB,OAAO,KAAK;;CAGnD,MAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,SAAS,IAAK,KAAI,aAAa,IAAI,KAAK;CAE1F,MAAM,MAAM,MAAM,YAAY,KAAK;EAClC,QAAQ,QAAQ;EAChB,UAAU;EACV,SAAS;GACR,UAAU;GACV,cAAc,iBAAiB;GAC/B,iBAAiB;GACjB,GAAI,QAAQ,WAAW;;EAGxB,IAAI;GACH,UAAU;GACV,iBAAiB;;EAElB,MAAM,QAAQ;;CAGf,MAAM,OAAO,MAAM,IAAI;CACvB,IAAIA;AACJ,KAAI;AACH,SAAO,KAAK,MAAM;SACX;AACP,SAAO;;AAGR,QAAO;EACN,IAAI,IAAI;EACR,YAAY,IAAI;EAChB,KAAK,IAAI;EACT,QAAQ,IAAI;EACZ,SAAS,IAAI;EACb;EACA;;;AAIF,SAAgB,cAAoB,MAAc;CACjD,MAAM,OAAO,KAAK,MAAM,iEAAiE;AACzF,KAAI,CAAC,KAAM,QAAO;AAElB,KAAI;AACH,SAAO,KAAK,MAAM;SACX;AACP,SAAO;;;AAKT,MAAa,iBAAiB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"core-XI62QUiR.js","names":["song: Song | null","info: RenderSongInfo | null","songdotlink: SongParser","applemusic: SongService","base: RenderInfoBase","soundcloud: SongService","base: RenderInfoBase","tracks: WidgetData[]","list: RenderInfoEntryBased[]","spotify: SongService","base: RenderInfoBase","list: RenderInfoEntryBased[]"],"sources":["../src/handlers/finders.ts","../src/handlers/defs/parsers/songdotlink.ts","../src/handlers/defs/cache.ts","../src/handlers/defs/services/applemusic.ts","../src/handlers/defs/services/soundcloud.ts","../src/handlers/defs/services/spotify.ts","../src/handlers/core.ts"],"sourcesContent":["import type { Song } from \"structs/types\";\r\n\r\nimport { clean } from \"./common\";\r\nimport type { RenderSongInfo, SongParser, SongService } from \"./helpers\";\r\n\r\n// introducing... a pointer! resolves the circular dependency\r\nexport const $ = {\r\n\tservices: [] as SongService[],\r\n\tparsers: [] as SongParser[],\r\n};\r\n\r\nfunction sid(song: Song) {\r\n\treturn [song.service, song.type, song.id].join(\":\");\r\n}\r\n\r\n// parseLink -> parseCache, validateCache\r\n// rebuildLink -> linkCache, validateCache\r\n// renderSong -> renderCache, validateCache\r\n// validateSong -> validateCache\r\n\r\nconst parseCache = new Map<string, Song | null>();\r\nconst validateCache = new Map<string, boolean>();\r\n/**\r\n * Tries to parse the provided **link**. Returns a **Song** if successful, or `null` if nothing was found. Either response is temporarily cached.\r\n * @example ```ts\r\n * await parseLink(\"https://soundcloud.com/c0ncernn\");\r\n * // { service: \"soundcloud\", type: \"user\", id: \"914653456\" }\r\n * ```\r\n */\r\nexport async function parseLink(link: string): Promise<Song | null> {\r\n\tconst cleaned = clean(link);\r\n\tif (parseCache.has(cleaned)) return parseCache.get(cleaned)!;\r\n\r\n\tconst { hostname, pathname } = new URL(cleaned);\r\n\tconst path = pathname.slice(1).split(/\\/+/);\r\n\r\n\tlet song: Song | null = null;\r\n\tfor (const parser of $.parsers) {\r\n\t\tif (parser.hosts.includes(hostname)) {\r\n\t\t\tsong = await parser.parse(cleaned, hostname, path);\r\n\t\t\tif (song) break;\r\n\t\t}\r\n\t}\r\n\r\n\tparseCache.set(cleaned, song);\r\n\tif (song) validateCache.set(sid(song), true);\r\n\treturn song;\r\n}\r\n\r\nconst linkCache = new Map<string, string | null>();\r\n/**\r\n * Tries to recreate the link to the provided **Song**. Returns `string` if successful, or `null` if nothing was found. Either response is temporarily cached.\r\n * @example ```ts\r\n * await parseLink({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\r\n * // https://soundcloud.com/c0ncernn\r\n * ```\r\n */\r\nexport async function rebuildLink(song: Song): Promise<string | null> {\r\n\tconst id = sid(song);\r\n\tif (linkCache.has(id)) return linkCache.get(id)!;\r\n\r\n\tlet link = null;\r\n\tconst service = $.services.find(x => x.name === song.service);\r\n\tif (service?.types.includes(song.type)) link = await service.rebuild(song.type, song.id);\r\n\r\n\tlinkCache.set(id, link);\r\n\tif (link) validateCache.set(id, true);\r\n\treturn link;\r\n}\r\n\r\nconst renderCache = new Map<string, RenderSongInfo | null>();\r\n/**\r\n * Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is temporarily cached.\r\n * @example ```ts\r\n * await renderSong({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\r\n * // { label: \"leroy\", sublabel: \"Top tracks\", explicit: false, form: \"list\", ... }\r\n * ```\r\n */\r\nexport async function renderSong(song: Song): Promise<RenderSongInfo | null> {\r\n\tconst id = sid(song);\r\n\tif (renderCache.has(id)) return renderCache.get(id)!;\r\n\r\n\tlet info: RenderSongInfo | null = null;\r\n\tconst service = $.services.find(x => x.name === song.service);\r\n\tif (service?.types.includes(song.type)) info = await service.render(song.type, song.id);\r\n\r\n\trenderCache.set(id, info);\r\n\tif (song) validateCache.set(sid(song), true);\r\n\treturn info;\r\n}\r\n\r\n/**\r\n * Validates if the provided **Song** exists. Returns a `boolean` depending on if the check was successful or not. Either response is temporarily cached.\r\n * @example ```ts\r\n * await renderSong({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\r\n * // true\r\n * ```\r\n */\r\nexport async function validateSong(song: Song): Promise<boolean> {\r\n\tconst id = sid(song);\r\n\tif (validateCache.has(id)) return validateCache.get(id)!;\r\n\r\n\tlet valid = false;\r\n\tconst service = $.services.find(x => x.name === song.service);\r\n\tif (service?.types.includes(song.type)) valid = await service.validate(song.type, song.id);\r\n\r\n\tvalidateCache.set(id, valid);\r\n\treturn valid;\r\n}\r\n\r\n/** Clears the cache for all handler functions */\r\nexport function clearCache() {\r\n\tparseCache.clear();\r\n\tlinkCache.clear();\r\n\trenderCache.clear();\r\n\tvalidateCache.clear();\r\n}\r\n","import { parseNextData, request } from \"handlers/common\";\r\nimport { parseLink } from \"handlers/finders\";\r\nimport { type SongParser } from \"handlers/helpers\";\r\n\r\ninterface Next {\r\n\tprops: {\r\n\t\tpageProps: {\r\n\t\t\tpageData: {\r\n\t\t\t\tsections: {\r\n\t\t\t\t\tlinks: {\r\n\t\t\t\t\t\tplatform: string;\r\n\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t}[];\r\n\t\t\t\t}[];\r\n\t\t\t};\r\n\t\t};\r\n\t};\r\n}\r\n\r\nexport const songdotlink: SongParser = {\r\n\tname: \"song.link\",\r\n\thosts: [\r\n\t\t\"song.link\",\r\n\t\t\"album.link\",\r\n\t\t\"artist.link\",\r\n\t\t\"pods.link\",\r\n\t\t\"playlist.link\",\r\n\t\t\"mylink.page\",\r\n\t\t\"odesli.co\",\r\n\t],\r\n\tasync parse(link, _host, path) {\r\n\t\tconst [first, second, third] = path;\r\n\t\tif (!first || third) return null;\r\n\r\n\t\tif (second && Number.isNaN(+second)) return null;\r\n\t\telse if (\r\n\t\t\t!second && (!first.match(/^[A-z0-9-_]+$/) || first.match(/^[-_]/) || first.match(/[-_]$/))\r\n\t\t) return null;\r\n\r\n\t\tconst html = (await request({\r\n\t\t\turl: link,\r\n\t\t})).text;\r\n\r\n\t\tconst sections = parseNextData<Next>(html)?.props?.pageProps?.pageData?.sections;\r\n\t\tif (!sections) return null;\r\n\r\n\t\tconst links = sections.flatMap(x => x.links ?? []).filter(x => x.url && x.platform);\r\n\r\n\t\tconst valid = links.find(x => x.platform === \"spotify\")\r\n\t\t\t?? links.find(x => x.platform === \"soundcloud\")\r\n\t\t\t?? links.find(x => x.platform === \"appleMusic\");\r\n\t\tif (!valid) return null;\r\n\r\n\t\treturn await parseLink(valid.url);\r\n\t},\r\n};\r\n","const handlerCache = new Map<string, unknown>();\r\n\r\nexport function makeCache<T, Args>(name: string, retrieve: (...args: Args[]) => T) {\r\n\treturn {\r\n\t\tretrieve(...args: Args[]) {\r\n\t\t\tif (handlerCache.has(name)) return handlerCache.get(name) as T;\r\n\r\n\t\t\tconst res = retrieve(...args);\r\n\t\t\tif (res instanceof Promise) {\r\n\t\t\t\treturn res.then((ret: T) => {\r\n\t\t\t\t\thandlerCache.set(name, ret);\r\n\t\t\t\t\treturn ret;\r\n\t\t\t\t});\r\n\t\t\t} else {\r\n\t\t\t\thandlerCache.set(name, res);\r\n\t\t\t\treturn res;\r\n\t\t\t}\r\n\t\t},\r\n\t};\r\n}\r\n","import { PLAYLIST_LIMIT, request } from \"handlers/common\";\r\nimport type { RenderInfoBase, SongService } from \"handlers/helpers\";\r\n\r\nimport { makeCache } from \"../cache\";\r\n\r\ninterface APIDataEntry {\r\n\tattributes: {\r\n\t\turl: string;\r\n\t\tname: string;\r\n\t\tartistName?: string;\r\n\t\tcontentRating?: \"explicit\";\r\n\t\tdurationInMillis?: number;\r\n\t\tpreviews?: {\r\n\t\t\turl: string;\r\n\t\t}[];\r\n\t\tartwork?: {\r\n\t\t\turl: string;\r\n\t\t};\r\n\t};\r\n\trelationships: {\r\n\t\tsongs?: {\r\n\t\t\tdata: APIDataEntry[];\r\n\t\t};\r\n\t\ttracks?: {\r\n\t\t\tdata: APIDataEntry[];\r\n\t\t};\r\n\t};\r\n}\r\n\r\ninterface APIData {\r\n\tdata: [APIDataEntry];\r\n}\r\n\r\nconst geo = \"us\", defaultName = \"songspotlight\";\r\n\r\nconst applemusicToken = makeCache(\"applemusicToken\", async (html?: string) => {\r\n\thtml ??= (await request({\r\n\t\turl: `https://music.apple.com/${geo}/new`,\r\n\t})).text;\r\n\r\n\tconst asset = html.match(/src=\"(\\/assets\\/index~\\w+\\.js)\"/i)?.[1];\r\n\tif (!asset) return;\r\n\r\n\tconst js = (await request({\r\n\t\turl: `https://music.apple.com${asset}`,\r\n\t})).text;\r\n\r\n\tconst code = js.match(/\\w+=\"(ey.*?)\"/i)?.[1];\r\n\treturn code;\r\n});\r\n\r\nexport const applemusic: SongService = {\r\n\tname: \"applemusic\",\r\n\thosts: [\r\n\t\t\"music.apple.com\",\r\n\t\t\"geo.music.apple.com\",\r\n\t],\r\n\ttypes: [\"artist\", \"album\", \"playlist\", \"song\"],\r\n\tasync parse(_link, _host, path) {\r\n\t\tconst [country, type, name, id, fourth] = path;\r\n\t\tif (!country || !type || !this.types.includes(type) || !name || !id || fourth) return null;\r\n\r\n\t\tconst res = await request({\r\n\t\t\turl: this.rebuild(type, id) as string,\r\n\t\t});\r\n\t\tif (res.status !== 200) return null;\r\n\r\n\t\tawait applemusicToken.retrieve(res.text);\r\n\r\n\t\treturn {\r\n\t\t\tservice: this.name,\r\n\t\t\ttype,\r\n\t\t\tid,\r\n\t\t};\r\n\t},\r\n\tasync render(type, id) {\r\n\t\tconst token = await applemusicToken.retrieve();\r\n\t\tif (!token) return null;\r\n\r\n\t\tconst res = await request({\r\n\t\t\turl: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s/${id}?include=songs`,\r\n\t\t\theaders: {\r\n\t\t\t\tauthorization: `Bearer ${token}`,\r\n\t\t\t\torigin: \"https://music.apple.com\",\r\n\t\t\t},\r\n\t\t});\r\n\t\tif (res.status !== 200) return null;\r\n\r\n\t\tconst { attributes, relationships } = (res.json as APIData).data[0];\r\n\r\n\t\tconst base: RenderInfoBase = {\r\n\t\t\tlabel: attributes.name,\r\n\t\t\tsublabel: attributes.artistName ?? \"Top Songs\",\r\n\t\t\texplicit: attributes.contentRating === \"explicit\",\r\n\t\t};\r\n\t\tconst thumbnailUrl = attributes.artwork?.url?.replace(/{[wh]}/g, \"128\");\r\n\r\n\t\tif (type === \"song\") {\r\n\t\t\tconst duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"single\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tsingle: {\r\n\t\t\t\t\taudio: previewUrl && duration\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tpreviewUrl,\r\n\t\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink: attributes.url,\r\n\t\t\t\t},\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tconst songs = (relationships.songs ?? relationships.tracks)?.data;\r\n\t\tif (!songs) return null;\r\n\r\n\t\treturn {\r\n\t\t\t...base,\r\n\t\t\tform: \"list\",\r\n\t\t\tthumbnailUrl,\r\n\t\t\tlist: songs.slice(0, PLAYLIST_LIMIT).map(({ attributes: song }) => {\r\n\t\t\t\tconst duration = song.durationInMillis, previewUrl = song.previews?.[0]?.url;\r\n\t\t\t\treturn {\r\n\t\t\t\t\tlabel: song.name,\r\n\t\t\t\t\tsublabel: song.artistName!,\r\n\t\t\t\t\texplicit: song.contentRating === \"explicit\",\r\n\t\t\t\t\taudio: previewUrl && duration\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tpreviewUrl,\r\n\t\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink: song.url,\r\n\t\t\t\t};\r\n\t\t\t}),\r\n\t\t};\r\n\t},\r\n\tasync validate(type, id) {\r\n\t\treturn (await request({\r\n\t\t\turl: this.rebuild(type, id) as string,\r\n\t\t})).status === 200;\r\n\t},\r\n\trebuild(type, id) {\r\n\t\treturn `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`;\r\n\t},\r\n};\r\n","import { PLAYLIST_LIMIT, request } from \"handlers/common\";\r\nimport { parseLink } from \"handlers/finders\";\r\nimport { type RenderInfoBase, type RenderInfoEntryBased, type SongService } from \"handlers/helpers\";\r\n\r\ninterface oEmbedData {\r\n\thtml: string;\r\n}\r\n\r\ninterface Transcoding {\r\n\tduration: number;\r\n\turl: string;\r\n\tformat: {\r\n\t\tprotocol: string;\r\n\t};\r\n}\r\n\r\ninterface WidgetData {\r\n\tartwork_url: string;\r\n\tavatar_url?: string;\r\n\ttitle: string;\r\n\tid: number;\r\n\tpermalink_url: string;\r\n\tusername?: string;\r\n\tuser?: {\r\n\t\tusername: string;\r\n\t};\r\n\tpublisher_metadata?: {\r\n\t\texplicit: boolean;\r\n\t};\r\n\tmedia?: {\r\n\t\ttranscodings: Transcoding[];\r\n\t};\r\n\ttracks?: WidgetData[];\r\n}\r\n\r\ninterface TracksWidgetData {\r\n\tcollection: WidgetData[];\r\n}\r\n\r\ninterface PreviewResponse {\r\n\turl: string;\r\n}\r\n\r\nconst client_id = \"nIjtjiYnjkOhMyh5xrbqEW12DxeJVnic\";\r\n\r\nfunction parseWidget(type: string, id: string, tracks: true): Promise<TracksWidgetData | undefined>;\r\nfunction parseWidget(type: string, id: string, tracks: false): Promise<WidgetData | undefined>;\r\nasync function parseWidget(type: string, id: string, tracks: boolean) {\r\n\treturn (await request({\r\n\t\turl: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? \"/tracks?limit=20\" : \"\"}`,\r\n\t\tquery: {\r\n\t\t\tclient_id,\r\n\t\t\t// app version isnt static but lets hope soundcloud doesnt mind :) :) :)\r\n\t\t\tapp_version: \"1764154491\",\r\n\t\t\tformat: \"json\",\r\n\t\t\trepresentation: \"full\",\r\n\t\t},\r\n\t})).json;\r\n}\r\nasync function parsePreview(transcodings: Transcoding[]) {\r\n\tconst preview = transcodings.sort((a, b) => {\r\n\t\tconst isA = a.format.protocol === \"progressive\";\r\n\t\tconst isB = b.format.protocol === \"progressive\";\r\n\r\n\t\treturn (isA && !isB) ? -1 : (isB && !isA) ? 1 : 0;\r\n\t})?.[0];\r\n\r\n\tif (preview?.url && preview?.duration) {\r\n\t\tconst link = (await request({\r\n\t\t\turl: preview.url,\r\n\t\t\tquery: {\r\n\t\t\t\tclient_id,\r\n\t\t\t},\r\n\t\t}))\r\n\t\t\t.json as PreviewResponse;\r\n\t\tif (!link?.url) return;\r\n\r\n\t\treturn {\r\n\t\t\tduration: preview.duration,\r\n\t\t\tpreviewUrl: link.url,\r\n\t\t};\r\n\t}\r\n}\r\n\r\nexport const soundcloud: SongService = {\r\n\tname: \"soundcloud\",\r\n\thosts: [\r\n\t\t\"soundcloud.com\",\r\n\t\t\"m.soundcloud.com\",\r\n\t\t\"on.soundcloud.com\",\r\n\t],\r\n\ttypes: [\"user\", \"track\", \"playlist\"],\r\n\tasync parse(link, host, path) {\r\n\t\tif (host === \"on.soundcloud.com\") {\r\n\t\t\tif (!path[0] || path[1]) return null;\r\n\t\t\tconst { url, status } = await request({\r\n\t\t\t\turl: link,\r\n\t\t\t});\r\n\t\t\treturn status === 200 ? await parseLink(url) : null;\r\n\t\t} else {\r\n\t\t\tconst [user, second, track, fourth] = path;\r\n\r\n\t\t\tlet valid = false;\r\n\t\t\tif (user && !second) valid = true; // user\r\n\t\t\telse if (user && second && second !== \"sets\" && !track) valid = true; // playlist\r\n\t\t\telse if (user && second === \"sets\" && track && !fourth) valid = true; // track\r\n\r\n\t\t\tif (!valid) return null;\r\n\r\n\t\t\tconst data = (await request({\r\n\t\t\t\turl: \"https://soundcloud.com/oembed\",\r\n\t\t\t\tquery: {\r\n\t\t\t\t\tformat: \"json\",\r\n\t\t\t\t\turl: link,\r\n\t\t\t\t},\r\n\t\t\t})).json as oEmbedData;\r\n\t\t\tif (!data?.html) return null;\r\n\r\n\t\t\t// https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F1053322828&show_artwork=true\r\n\t\t\tconst rawUrl = data.html.match(/w\\.soundcloud\\.com.*?url=(.*?)[&\"]/)?.[1];\r\n\t\t\tif (!rawUrl) return null;\r\n\r\n\t\t\t// https://api.soundcloud.com/tracks/1053322828\r\n\t\t\tconst splits = decodeURIComponent(rawUrl).split(/\\/+/);\r\n\t\t\tconst kind = splits[2], id = splits[3];\r\n\t\t\tif (!kind || !id) return null;\r\n\r\n\t\t\treturn {\r\n\t\t\t\tservice: this.name,\r\n\t\t\t\ttype: kind.slice(0, -1), // turns tracks -> track\r\n\t\t\t\tid,\r\n\t\t\t};\r\n\t\t}\r\n\t},\r\n\tasync render(type, id) {\r\n\t\tconst data = await parseWidget(type, id, false);\r\n\t\tif (!data?.id) return null;\r\n\r\n\t\tconst base: RenderInfoBase = {\r\n\t\t\tlabel: data.title ?? data.username,\r\n\t\t\tsublabel: data.user?.username ?? \"Top tracks\",\r\n\t\t\texplicit: Boolean(data.publisher_metadata?.explicit),\r\n\t\t};\r\n\t\tconst thumbnailUrl = data.artwork_url ?? data.avatar_url;\r\n\r\n\t\tif (type === \"track\") {\r\n\t\t\tconst audio = await parsePreview(data.media?.transcodings ?? []);\r\n\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"single\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tsingle: {\r\n\t\t\t\t\taudio,\r\n\t\t\t\t\tlink: data.permalink_url,\r\n\t\t\t\t},\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tlet tracks: WidgetData[] = [];\r\n\t\tif (type === \"user\") {\r\n\t\t\tconst got = await parseWidget(type, id, true);\r\n\t\t\tif (!got?.collection) return null;\r\n\r\n\t\t\ttracks = got.collection;\r\n\t\t} else {\r\n\t\t\tif (!data.tracks) return null;\r\n\r\n\t\t\ttracks = data.tracks;\r\n\t\t}\r\n\r\n\t\tconst list: RenderInfoEntryBased[] = [];\r\n\t\tfor (const track of tracks) {\r\n\t\t\t// unavailable songs\r\n\t\t\tif (!track.title || list.length >= PLAYLIST_LIMIT) continue;\r\n\r\n\t\t\tconst audio = await parsePreview(track.media?.transcodings ?? []);\r\n\r\n\t\t\tlist.push({\r\n\t\t\t\tlabel: track.title,\r\n\t\t\t\tsublabel: track.user?.username ?? \"IDKK\",\r\n\t\t\t\texplicit: Boolean(track.publisher_metadata!.explicit),\r\n\t\t\t\taudio,\r\n\t\t\t\tlink: track.permalink_url,\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\treturn {\r\n\t\t\t...base,\r\n\t\t\tform: \"list\",\r\n\t\t\tthumbnailUrl,\r\n\t\t\tlist,\r\n\t\t};\r\n\t},\r\n\tasync validate(type, id) {\r\n\t\treturn (await parseWidget(type, id, false))?.id !== undefined;\r\n\t},\r\n\tasync rebuild(type, id) {\r\n\t\treturn (await parseWidget(type, id, false))?.permalink_url ?? null;\r\n\t},\r\n};\r\n","import { parseNextData, PLAYLIST_LIMIT, request } from \"handlers/common\";\r\nimport { type RenderInfoBase, type RenderInfoEntryBased, type SongService } from \"handlers/helpers\";\r\n\r\ninterface Next {\r\n\tprops: {\r\n\t\tpageProps: {\r\n\t\t\ttitle?: string;\r\n\t\t\tstate?: {\r\n\t\t\t\tdata: {\r\n\t\t\t\t\tentity: {\r\n\t\t\t\t\t\turi: string;\r\n\t\t\t\t\t\ttitle: string;\r\n\t\t\t\t\t\tsubtitle: string;\r\n\t\t\t\t\t\tisExplicit: boolean;\r\n\t\t\t\t\t\tartists?: {\r\n\t\t\t\t\t\t\tname: string;\r\n\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\tduration?: number;\r\n\t\t\t\t\t\taudioPreview?: {\r\n\t\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t\ttrackList?: {\r\n\t\t\t\t\t\t\turi: string;\r\n\t\t\t\t\t\t\ttitle: string;\r\n\t\t\t\t\t\t\tsubtitle: string;\r\n\t\t\t\t\t\t\tisExplicit: boolean;\r\n\t\t\t\t\t\t\tartists?: {\r\n\t\t\t\t\t\t\t\tname: string;\r\n\t\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\t\tduration?: number;\r\n\t\t\t\t\t\t\taudioPreview?: {\r\n\t\t\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t\t\t};\r\n\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\tvisualIdentity: {\r\n\t\t\t\t\t\t\timage: {\r\n\t\t\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t\t\t\tmaxWidth: number;\r\n\t\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t};\r\n\t\t\t\t};\r\n\t\t\t};\r\n\t\t};\r\n\t};\r\n}\r\n\r\nasync function parseEmbed(type: string, id: string) {\r\n\treturn parseNextData<Next>(\r\n\t\t(await request({\r\n\t\t\turl: `https://open.spotify.com/embed/${type}/${id}`,\r\n\t\t})).text,\r\n\t);\r\n}\r\n\r\nfunction fromUri(uri: string) {\r\n\tconst [sanityCheck, type, id] = uri.split(\":\");\r\n\tif (sanityCheck === \"spotify\" && type && id) return `https://open.spotify.com/${type}/${id}`;\r\n\telse return null;\r\n}\r\n\r\nexport const spotify: SongService = {\r\n\tname: \"spotify\",\r\n\thosts: [\r\n\t\t\"open.spotify.com\",\r\n\t],\r\n\ttypes: [\"track\", \"album\", \"playlist\", \"artist\"],\r\n\tasync parse(_link, _host, path) {\r\n\t\tconst [type, id, third] = path;\r\n\t\tif (!type || !this.types.includes(type as never) || !id || third) return null;\r\n\r\n\t\tif (!await this.validate(type, id)) return null;\r\n\r\n\t\treturn {\r\n\t\t\tservice: this.name,\r\n\t\t\ttype,\r\n\t\t\tid,\r\n\t\t};\r\n\t},\r\n\tasync render(type, id) {\r\n\t\tconst data = (await parseEmbed(type, id) as Next)?.props?.pageProps?.state?.data?.entity;\r\n\t\tif (!data) return null;\r\n\r\n\t\tconst base: RenderInfoBase = {\r\n\t\t\tlabel: data.title,\r\n\t\t\tsublabel: data.subtitle ?? data.artists?.map(x => x.name).join(\", \"),\r\n\t\t\texplicit: Boolean(data.isExplicit),\r\n\t\t};\r\n\t\tconst thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]\r\n\t\t\t?.url.replace(/:\\/\\/.*?\\.spotifycdn\\.com\\/image/, \"://i.scdn.co/image\");\r\n\r\n\t\tif (type === \"track\") {\r\n\t\t\tconst link = fromUri(data.uri)!;\r\n\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"single\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tsingle: {\r\n\t\t\t\t\taudio: (data.audioPreview && data.duration)\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tduration: data.duration,\r\n\t\t\t\t\t\t\tpreviewUrl: data.audioPreview.url,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink,\r\n\t\t\t\t},\r\n\t\t\t};\r\n\t\t} else {\r\n\t\t\tconst list: RenderInfoEntryBased[] = [];\r\n\t\t\tfor (const track of (data.trackList ?? [])) {\r\n\t\t\t\tif (list.length >= PLAYLIST_LIMIT) continue;\r\n\t\t\t\tconst link = fromUri(track.uri)!;\r\n\r\n\t\t\t\tlist.push({\r\n\t\t\t\t\tlabel: track.title,\r\n\t\t\t\t\tsublabel: track.subtitle ?? track.artists?.map(x => x.name).join(\", \"),\r\n\t\t\t\t\texplicit: Boolean(track.isExplicit),\r\n\t\t\t\t\taudio: (track.audioPreview && track.duration)\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tduration: track.duration,\r\n\t\t\t\t\t\t\tpreviewUrl: track.audioPreview.url,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink,\r\n\t\t\t\t});\r\n\t\t\t}\r\n\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"list\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tlist,\r\n\t\t\t};\r\n\t\t}\r\n\t},\r\n\tasync validate(type, id) {\r\n\t\treturn !(await parseEmbed(type, id))?.props?.pageProps?.title;\r\n\t},\r\n\trebuild(type, id) {\r\n\t\treturn `https://open.spotify.com/${type}/${id}`;\r\n\t},\r\n};\r\n","import { songdotlink } from \"./defs/parsers/songdotlink\";\r\nimport { applemusic } from \"./defs/services/applemusic\";\r\nimport { soundcloud } from \"./defs/services/soundcloud\";\r\nimport { spotify } from \"./defs/services/spotify\";\r\nimport { $ } from \"./finders\";\r\nimport type { SongParser, SongService } from \"./helpers\";\r\n\r\nexport const services = [\r\n\tspotify,\r\n\tsoundcloud,\r\n\tapplemusic,\r\n] as SongService[];\r\n$.services = services;\r\n\r\nexport const parsers = [\r\n\tsongdotlink,\r\n\t...services,\r\n] as SongParser[];\r\n$.parsers = parsers;\r\n"],"mappings":"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"}