@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 +30 -7
- package/dist/{core-Bcywz03y.js → core-BvANu1gt.js} +84 -208
- package/dist/core-BvANu1gt.js.map +1 -0
- package/dist/finders-8eyiJuIz.js +150 -0
- package/dist/finders-8eyiJuIz.js.map +1 -0
- package/dist/handlers.d.ts +5 -13
- package/dist/handlers.js +3 -3
- package/dist/structs.d.ts +1 -1
- package/dist/structs.js +1 -2
- package/dist/structs.js.map +1 -1
- package/dist/{types-B2sGtUCQ.d.ts → types-DRQ6d925.d.ts} +1 -1
- package/dist/util.d.ts +30 -2
- package/dist/util.js +39 -2
- package/dist/util.js.map +1 -0
- package/package.json +52 -50
- package/dist/common-BYkNs_NK.js +0 -78
- package/dist/common-BYkNs_NK.js.map +0 -1
- package/dist/core-Bcywz03y.js.map +0 -1
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
216
|
-
const duration =
|
|
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:
|
|
219
|
-
sublabel:
|
|
220
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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 {
|
|
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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"}
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Song } from "./types-
|
|
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
|
-
}
|
|
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,
|
|
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 "./
|
|
2
|
-
import {
|
|
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,
|
|
4
|
+
export { $, clearCache, parseLink, parsers, renderSong, services, validateSong };
|
package/dist/structs.d.ts
CHANGED
package/dist/structs.js
CHANGED
package/dist/structs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structs.js","names":[],"sources":["../src/structs/zod.ts"],"sourcesContent":["import { services } from \"handlers/core\";\
|
|
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"}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 "./
|
|
1
|
+
import { l as setFetchHandler, t as $ } from "./finders-8eyiJuIz.js";
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|
package/dist/util.js.map
ADDED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
package/dist/common-BYkNs_NK.js
DELETED
|
@@ -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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"}
|