@song-spotlight/api 1.1.3 → 1.2.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region package.json
|
|
2
|
-
var version = "1.
|
|
2
|
+
var version = "1.2.0";
|
|
3
3
|
|
|
4
4
|
//#endregion
|
|
5
5
|
//#region src/handlers/common.ts
|
|
@@ -91,6 +91,17 @@ async function parseLink(link) {
|
|
|
91
91
|
if (song) validateCache.set(sid(song), true);
|
|
92
92
|
return song;
|
|
93
93
|
}
|
|
94
|
+
const linkCache = /* @__PURE__ */ new Map();
|
|
95
|
+
async function rebuildLink(song) {
|
|
96
|
+
const id = sid(song);
|
|
97
|
+
if (linkCache.has(id)) return linkCache.get(id);
|
|
98
|
+
let link = null;
|
|
99
|
+
const service = $.services.find((x) => x.name === song.service);
|
|
100
|
+
if (service?.types.includes(song.type)) link = await service.rebuild(song.type, song.id);
|
|
101
|
+
linkCache.set(id, link);
|
|
102
|
+
if (link) validateCache.set(id, true);
|
|
103
|
+
return link;
|
|
104
|
+
}
|
|
94
105
|
const renderCache = /* @__PURE__ */ new Map();
|
|
95
106
|
async function renderSong(song) {
|
|
96
107
|
const id = sid(song);
|
|
@@ -160,7 +171,7 @@ function makeCache(name, retrieve) {
|
|
|
160
171
|
|
|
161
172
|
//#endregion
|
|
162
173
|
//#region src/handlers/defs/services/applemusic.ts
|
|
163
|
-
const geo = "
|
|
174
|
+
const geo = "us", defaultName = "songspotlight";
|
|
164
175
|
const applemusicToken = makeCache("applemusicToken", async (html) => {
|
|
165
176
|
html ??= (await request({ url: `https://music.apple.com/${geo}/new` })).text;
|
|
166
177
|
const asset = html.match(/src="(\/assets\/index~\w+\.js)"/i)?.[1];
|
|
@@ -181,7 +192,7 @@ const applemusic = {
|
|
|
181
192
|
async parse(_link, _host, path) {
|
|
182
193
|
const [country, type, name, id, fourth] = path;
|
|
183
194
|
if (!country || !type || !this.types.includes(type) || !name || !id || fourth) return null;
|
|
184
|
-
const res = await request({ url:
|
|
195
|
+
const res = await request({ url: this.rebuild(type, id) });
|
|
185
196
|
if (res.status !== 200) return null;
|
|
186
197
|
await applemusicToken.retrieve(res.text);
|
|
187
198
|
return {
|
|
@@ -207,7 +218,7 @@ const applemusic = {
|
|
|
207
218
|
sublabel: attributes.artistName ?? "Top Songs",
|
|
208
219
|
explicit: attributes.contentRating === "explicit"
|
|
209
220
|
};
|
|
210
|
-
const thumbnailUrl = attributes.artwork?.url?.replace(
|
|
221
|
+
const thumbnailUrl = attributes.artwork?.url?.replace(/{[wh]}/g, "128");
|
|
211
222
|
if (type === "song") {
|
|
212
223
|
const duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;
|
|
213
224
|
return {
|
|
@@ -245,7 +256,10 @@ const applemusic = {
|
|
|
245
256
|
};
|
|
246
257
|
},
|
|
247
258
|
async validate(type, id) {
|
|
248
|
-
return (await request({ url:
|
|
259
|
+
return (await request({ url: this.rebuild(type, id) })).status === 200;
|
|
260
|
+
},
|
|
261
|
+
rebuild(type, id) {
|
|
262
|
+
return `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`;
|
|
249
263
|
}
|
|
250
264
|
};
|
|
251
265
|
|
|
@@ -257,7 +271,7 @@ async function parseWidget(type, id, tracks) {
|
|
|
257
271
|
url: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? "/tracks?limit=20" : ""}`,
|
|
258
272
|
query: {
|
|
259
273
|
client_id,
|
|
260
|
-
app_version: "
|
|
274
|
+
app_version: "1764154491",
|
|
261
275
|
format: "json",
|
|
262
276
|
representation: "full"
|
|
263
277
|
}
|
|
@@ -349,7 +363,7 @@ const soundcloud = {
|
|
|
349
363
|
let tracks = [];
|
|
350
364
|
if (type === "user") {
|
|
351
365
|
const got = await parseWidget(type, id, true);
|
|
352
|
-
if (!got
|
|
366
|
+
if (!got?.collection) return null;
|
|
353
367
|
tracks = got.collection;
|
|
354
368
|
} else {
|
|
355
369
|
if (!data.tracks) return null;
|
|
@@ -376,6 +390,9 @@ const soundcloud = {
|
|
|
376
390
|
},
|
|
377
391
|
async validate(type, id) {
|
|
378
392
|
return (await parseWidget(type, id, false))?.id !== void 0;
|
|
393
|
+
},
|
|
394
|
+
async rebuild(type, id) {
|
|
395
|
+
return (await parseWidget(type, id, false))?.permalink_url ?? null;
|
|
379
396
|
}
|
|
380
397
|
};
|
|
381
398
|
|
|
@@ -457,6 +474,9 @@ const spotify = {
|
|
|
457
474
|
},
|
|
458
475
|
async validate(type, id) {
|
|
459
476
|
return !(await parseEmbed(type, id))?.props?.pageProps?.title;
|
|
477
|
+
},
|
|
478
|
+
rebuild(type, id) {
|
|
479
|
+
return `https://open.spotify.com/${type}/${id}`;
|
|
460
480
|
}
|
|
461
481
|
};
|
|
462
482
|
|
|
@@ -472,4 +492,4 @@ const parsers = [songdotlink, ...services];
|
|
|
472
492
|
$.parsers = parsers;
|
|
473
493
|
|
|
474
494
|
//#endregion
|
|
475
|
-
export { $, parseLink, parsers, renderSong, services, validateSong };
|
|
495
|
+
export { $, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-D5Z1B1Ub.js","names":["json: unknown","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":["../package.json","../src/handlers/common.ts","../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":["{\n\t\"name\": \"@song-spotlight/api\",\n\t\"version\": \"1.2.0\",\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},\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\"dependencies\": {\n\t\t\"zod\": \"^4.1.5\"\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}\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\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 fetch(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\": `song-spotlight/v${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","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\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\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\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\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","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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"}
|
package/dist/handlers.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface SongService extends SongParser {
|
|
|
11
11
|
types: string[];
|
|
12
12
|
render(type: string, id: string): MaybePromise<RenderSongInfo | null>;
|
|
13
13
|
validate(type: string, id: string): MaybePromise<boolean>;
|
|
14
|
+
rebuild(type: string, id: string): MaybePromise<string | null>;
|
|
14
15
|
}
|
|
15
16
|
interface RenderInfoBase {
|
|
16
17
|
label: string;
|
|
@@ -47,7 +48,8 @@ declare const $: {
|
|
|
47
48
|
parsers: SongParser[];
|
|
48
49
|
};
|
|
49
50
|
declare function parseLink(link: string): Promise<Song | null>;
|
|
51
|
+
declare function rebuildLink(song: Song): Promise<string | null>;
|
|
50
52
|
declare function renderSong(song: Song): Promise<RenderSongInfo | null>;
|
|
51
53
|
declare function validateSong(song: Song): Promise<boolean>;
|
|
52
54
|
//#endregion
|
|
53
|
-
export { $, RenderInfoBase, RenderInfoEntry, RenderInfoEntryBased, RenderSongInfo, SongParser, SongService, parseLink, parsers, renderSong, services, validateSong };
|
|
55
|
+
export { $, RenderInfoBase, RenderInfoEntry, RenderInfoEntryBased, RenderSongInfo, SongParser, SongService, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
|
package/dist/handlers.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { $, parseLink, parsers, renderSong, services, validateSong } from "./core-
|
|
1
|
+
import { $, parseLink, parsers, rebuildLink, renderSong, services, validateSong } from "./core-D5Z1B1Ub.js";
|
|
2
2
|
|
|
3
|
-
export { $, parseLink, parsers, renderSong, services, validateSong };
|
|
3
|
+
export { $, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
|
package/dist/structs.js
CHANGED
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core-DrHF9uHh.js","names":["json: unknown","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":["../package.json","../src/handlers/common.ts","../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":["{\n\t\"name\": \"@song-spotlight/api\",\n\t\"version\": \"1.1.3\",\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},\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\"dependencies\": {\n\t\t\"zod\": \"^4.1.5\"\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}\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\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 fetch(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\": `song-spotlight/v${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","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\nconst parseCache = new Map<string, Song | null>();\r\nconst validateCache = new Map<string, boolean>();\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 renderCache = new Map<string, RenderSongInfo | null>();\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\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","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\n// why not, right\r\nconst geo = \"fr\", 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: `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`,\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(\"{w}\", \"128\")?.replace(\"{h}\", \"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: `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`,\r\n\t\t})).status === 200;\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\nasync function parseWidget<Tracks extends boolean>(type: string, id: string, tracks: Tracks) {\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: \"1752674865\",\r\n\t\t\tformat: \"json\",\r\n\t\t\trepresentation: \"full\",\r\n\t\t},\r\n\t})).json as Tracks extends true ? TracksWidgetData : WidgetData;\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};\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};\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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"}
|