@song-spotlight/api 1.3.1 → 2.0.0

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,9 @@
1
- import { PLAYLIST_LIMIT, clean, parseNextData, request } from "./common-BYkNs_NK.js";
1
+ import { c as request, o as PLAYLIST_LIMIT, r as parseLink, s as parseNextData, t as $ } from "./finders-8eyiJuIz.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
100
4
  const songdotlink = {
101
5
  name: "song.link",
6
+ label: "song.link",
102
7
  hosts: [
103
8
  "song.link",
104
9
  "album.link",
@@ -144,16 +49,18 @@ function makeCache(name, retrieve) {
144
49
  //#endregion
145
50
  //#region src/handlers/defs/services/applemusic.ts
146
51
  const geo = "us", defaultName = "songspotlight";
52
+ function applemusicLink(type, id) {
53
+ return `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`;
54
+ }
147
55
  const applemusicToken = makeCache("applemusicToken", async (html) => {
148
56
  html ??= (await request({ url: `https://music.apple.com/${geo}/new` })).text;
149
57
  const asset = html.match(/src="(\/assets\/index~\w+\.js)"/i)?.[1];
150
58
  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;
59
+ return (await request({ url: `https://music.apple.com${asset}` })).text.match(/\w+="(ey.*?)"/i)?.[1];
154
60
  });
155
61
  const applemusic = {
156
62
  name: "applemusic",
63
+ label: "Apple Music",
157
64
  hosts: ["music.apple.com", "geo.music.apple.com"],
158
65
  types: [
159
66
  "artist",
@@ -164,7 +71,7 @@ const applemusic = {
164
71
  async parse(_link, _host, path) {
165
72
  const [country, type, name, id, fourth] = path;
166
73
  if (!country || !type || !this.types.includes(type) || !name || !id || fourth) return null;
167
- const res = await request({ url: this.rebuild(type, id) });
74
+ const res = await request({ url: applemusicLink(type, id) });
168
75
  if (res.status !== 200) return null;
169
76
  await applemusicToken.retrieve(res.text);
170
77
  return {
@@ -177,7 +84,11 @@ const applemusic = {
177
84
  const token = await applemusicToken.retrieve();
178
85
  if (!token) return null;
179
86
  const res = await request({
180
- url: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s/${id}?include=songs`,
87
+ url: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s`,
88
+ query: {
89
+ include: "songs",
90
+ ids: id
91
+ },
181
92
  headers: {
182
93
  authorization: `Bearer ${token}`,
183
94
  origin: "https://music.apple.com"
@@ -187,51 +98,43 @@ const applemusic = {
187
98
  const { attributes, relationships } = res.json.data[0];
188
99
  const base = {
189
100
  label: attributes.name,
190
- sublabel: attributes.artistName ?? "Top Songs",
101
+ sublabel: attributes.artistName ?? "Top songs",
102
+ link: attributes.url,
191
103
  explicit: attributes.contentRating === "explicit"
192
104
  };
193
105
  const thumbnailUrl = attributes.artwork?.url?.replace(/{[wh]}/g, "128");
194
106
  if (type === "song") {
195
107
  const duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;
196
108
  return {
197
- ...base,
198
109
  form: "single",
110
+ ...base,
199
111
  thumbnailUrl,
200
- single: {
201
- audio: previewUrl && duration ? {
202
- previewUrl,
203
- duration
204
- } : void 0,
205
- link: attributes.url
206
- }
112
+ single: { audio: previewUrl && duration ? {
113
+ previewUrl,
114
+ duration
115
+ } : void 0 }
207
116
  };
208
- }
209
- const songs = (relationships.songs ?? relationships.tracks)?.data;
210
- if (!songs) return null;
211
- return {
212
- ...base,
117
+ } else return {
213
118
  form: "list",
119
+ ...base,
214
120
  thumbnailUrl,
215
- list: songs.slice(0, PLAYLIST_LIMIT).map(({ attributes: song }) => {
216
- const duration = song.durationInMillis, previewUrl = song.previews?.[0]?.url;
121
+ list: (relationships.tracks?.data ?? relationships.songs?.data ?? []).slice(0, PLAYLIST_LIMIT).map(({ attributes }) => {
122
+ const duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;
217
123
  return {
218
- label: song.name,
219
- sublabel: song.artistName,
220
- explicit: song.contentRating === "explicit",
124
+ label: attributes.name,
125
+ sublabel: attributes.artistName,
126
+ link: attributes.url,
127
+ explicit: attributes.contentRating === "explicit",
221
128
  audio: previewUrl && duration ? {
222
129
  previewUrl,
223
130
  duration
224
- } : void 0,
225
- link: song.url
131
+ } : void 0
226
132
  };
227
133
  })
228
134
  };
229
135
  },
230
136
  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}`;
137
+ return (await request({ url: applemusicLink(type, id) })).status === 200;
235
138
  }
236
139
  };
237
140
 
@@ -240,12 +143,12 @@ const applemusic = {
240
143
  const client_id = "nIjtjiYnjkOhMyh5xrbqEW12DxeJVnic";
241
144
  async function parseWidget(type, id, tracks) {
242
145
  return (await request({
243
- url: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? "/tracks?limit=20" : ""}`,
146
+ url: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? "/tracks" : ""}`,
244
147
  query: {
245
- client_id,
246
- app_version: "1764154491",
247
148
  format: "json",
248
- representation: "full"
149
+ client_id,
150
+ app_version: "1768986291",
151
+ limit: "20"
249
152
  }
250
153
  })).json;
251
154
  }
@@ -269,6 +172,7 @@ async function parsePreview(transcodings) {
269
172
  }
270
173
  const soundcloud = {
271
174
  name: "soundcloud",
175
+ label: "Soundcloud",
272
176
  hosts: [
273
177
  "soundcloud.com",
274
178
  "m.soundcloud.com",
@@ -317,54 +221,40 @@ const soundcloud = {
317
221
  const base = {
318
222
  label: data.title ?? data.username,
319
223
  sublabel: data.user?.username ?? "Top tracks",
224
+ link: data.permalink_url,
320
225
  explicit: Boolean(data.publisher_metadata?.explicit)
321
226
  };
322
227
  const thumbnailUrl = data.artwork_url ?? data.avatar_url;
323
228
  if (type === "track") {
324
- const audio = await parsePreview(data.media?.transcodings ?? []);
229
+ const audio = await parsePreview(data.media?.transcodings ?? []).catch(() => void 0);
325
230
  return {
326
- ...base,
327
231
  form: "single",
232
+ ...base,
328
233
  thumbnailUrl,
329
- single: {
330
- audio,
331
- link: data.permalink_url
332
- }
234
+ single: { audio }
333
235
  };
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
236
  } 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
- });
237
+ let tracks = [];
238
+ if (type === "user") {
239
+ const got = await parseWidget(type, id, true).catch(() => void 0);
240
+ if (got?.collection) tracks = got.collection;
241
+ } else if (data.tracks) tracks = data.tracks;
242
+ return {
243
+ form: "list",
244
+ ...base,
245
+ thumbnailUrl,
246
+ list: await Promise.all(tracks.filter((x) => x.title).slice(0, PLAYLIST_LIMIT).map(async (track) => ({
247
+ label: track.title,
248
+ sublabel: track.user?.username ?? "unknown",
249
+ link: track.permalink_url,
250
+ explicit: Boolean(track.publisher_metadata.explicit),
251
+ audio: await parsePreview(track.media?.transcodings ?? []).catch(() => void 0)
252
+ })))
253
+ };
355
254
  }
356
- return {
357
- ...base,
358
- form: "list",
359
- thumbnailUrl,
360
- list
361
- };
362
255
  },
363
256
  async validate(type, id) {
364
257
  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
258
  }
369
259
  };
370
260
 
@@ -380,6 +270,7 @@ function fromUri(uri) {
380
270
  }
381
271
  const spotify = {
382
272
  name: "spotify",
273
+ label: "Spotify",
383
274
  hosts: ["open.spotify.com"],
384
275
  types: [
385
276
  "track",
@@ -403,52 +294,37 @@ const spotify = {
403
294
  const base = {
404
295
  label: data.title,
405
296
  sublabel: data.subtitle ?? data.artists?.map((x) => x.name).join(", "),
297
+ link: fromUri(data.uri),
406
298
  explicit: Boolean(data.isExplicit)
407
299
  };
408
- const thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]?.url;
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
- }
300
+ const thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]?.url.replace(/:\/\/.*?\.spotifycdn\.com\/image/, "://i.scdn.co/image");
301
+ if (type === "track") return {
302
+ form: "single",
303
+ ...base,
304
+ thumbnailUrl,
305
+ single: { audio: data.audioPreview && data.duration ? {
306
+ duration: data.duration,
307
+ previewUrl: data.audioPreview.url
308
+ } : void 0 }
309
+ };
310
+ else return {
311
+ form: "list",
312
+ ...base,
313
+ thumbnailUrl,
314
+ list: (data.trackList ?? []).slice(0, PLAYLIST_LIMIT).map((track) => ({
315
+ label: track.title,
316
+ sublabel: track.subtitle ?? track.artists?.map((x) => x.name).join(", "),
317
+ link: fromUri(track.uri),
318
+ explicit: Boolean(track.isExplicit),
319
+ audio: track.audioPreview && track.duration ? {
320
+ duration: track.duration,
321
+ previewUrl: track.audioPreview.url
322
+ } : void 0
323
+ }))
324
+ };
446
325
  },
447
326
  async validate(type, id) {
448
327
  return !(await parseEmbed(type, id))?.props?.pageProps?.title;
449
- },
450
- rebuild(type, id) {
451
- return `https://open.spotify.com/${type}/${id}`;
452
328
  }
453
329
  };
454
330
 
@@ -464,4 +340,4 @@ const parsers = [songdotlink, ...services];
464
340
  $.parsers = parsers;
465
341
 
466
342
  //#endregion
467
- export { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
343
+ export { services as n, parsers as t };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-BvANu1gt.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\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 && Number.isNaN(+second)) return null;\n\t\telse if (\n\t\t\t!second && (!first.match(/^[A-z0-9-_]+$/) || first.match(/^[-_]/) || first.match(/[-_]$/))\n\t\t) 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.0";
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-8eyiJuIz.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-BYkNs_NK.js";
2
- import { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong } from "./core-Bcywz03y.js";
1
+ import { a as validateSong, i as renderSong, n as clearCache, r as parseLink, t as $ } from "./finders-8eyiJuIz.js";
2
+ import { n as services, t as parsers } from "./core-BvANu1gt.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-BYkNs_NK.js";
2
- import { services } from "./core-Bcywz03y.js";
1
+ import { n as services } from "./core-BvANu1gt.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-BYkNs_NK.js";
1
+ import { l as setFetchHandler, t as $ } from "./finders-8eyiJuIz.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.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.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.0",
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.1";
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-BYkNs_NK.js","names":["json: unknown"],"sources":["../package.json","../src/handlers/common.ts"],"sourcesContent":["{\n\t\"name\": \"@song-spotlight/api\",\n\t\"version\": \"1.3.1\",\n\t\"description\": \"Song Spotlight API types and song validation module\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"build\": \"rm -rf dist && rolldown -c rolldown.config.mts\",\n\t\t\"prepublishOnly\": \"bun run build\"\n\t},\n\t\"exports\": {\n\t\t\"./handlers\": {\n\t\t\t\"types\": \"./dist/handlers.d.ts\",\n\t\t\t\"default\": \"./dist/handlers.js\"\n\t\t},\n\t\t\"./structs\": {\n\t\t\t\"types\": \"./dist/structs.d.ts\",\n\t\t\t\"default\": \"./dist/structs.js\"\n\t\t},\n\t\t\"./util\": {\n\t\t\t\"types\": \"./dist/util.d.ts\",\n\t\t\t\"default\": \"./dist/util.js\"\n\t\t}\n\t},\n\t\"publishConfig\": {\n\t\t\"access\": \"public\"\n\t},\n\t\"files\": [\n\t\t\"dist\"\n\t],\n\t\"license\": \"MIT\",\n\t\"homepage\": \"https://github.com/nexpid-labs/SongSpotlight/tree/main/packages/api\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/nexpid-labs/SongSpotlight.git\",\n\t\t\"directory\": \"packages/api\"\n\t},\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/nexpid-labs/SongSpotlight/issues\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@types/bun\": \"latest\",\n\t\t\"rolldown\": \"^1.0.0-beta.34\",\n\t\t\"rolldown-plugin-dts\": \"^0.15.10\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"typescript\": \"^5\"\n\t},\n\t\"optionalDependencies\": {\n\t\t\"zod\": \"^4.3.6\"\n\t}\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":";cAEY;;;;ACQZ,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-Bcywz03y.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]?.url;\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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"}