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