@song-spotlight/api 1.3.0 → 1.3.2
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/dist/{common-PYa2ZOr8.js → common-DrSlxvDY.js} +1 -1
- package/dist/common-DrSlxvDY.js.map +1 -0
- package/dist/{core-CbgoQ0eP.js → core-XI62QUiR.js} +2 -2
- package/dist/{core-CbgoQ0eP.js.map → core-XI62QUiR.js.map} +1 -1
- package/dist/handlers.d.ts +1 -1
- package/dist/handlers.js +2 -2
- package/dist/structs.js +2 -2
- package/dist/util.js +1 -1
- package/package.json +50 -50
- package/dist/common-PYa2ZOr8.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { PLAYLIST_LIMIT, clean, parseNextData, request } from "./common-
|
|
1
|
+
import { PLAYLIST_LIMIT, clean, parseNextData, request } from "./common-DrSlxvDY.js";
|
|
2
2
|
|
|
3
3
|
//#region src/handlers/finders.ts
|
|
4
4
|
const $ = {
|
|
@@ -405,7 +405,7 @@ const spotify = {
|
|
|
405
405
|
sublabel: data.subtitle ?? data.artists?.map((x) => x.name).join(", "),
|
|
406
406
|
explicit: Boolean(data.isExplicit)
|
|
407
407
|
};
|
|
408
|
-
const thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]?.url;
|
|
408
|
+
const thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]?.url.replace(/:\/\/.*?\.spotifycdn\.com\/image/, "://i.scdn.co/image");
|
|
409
409
|
if (type === "track") {
|
|
410
410
|
const link = fromUri(data.uri);
|
|
411
411
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core-CbgoQ0eP.js","names":["song: Song | null","info: RenderSongInfo | null","songdotlink: SongParser","applemusic: SongService","base: RenderInfoBase","soundcloud: SongService","base: RenderInfoBase","tracks: WidgetData[]","list: RenderInfoEntryBased[]","spotify: SongService","base: RenderInfoBase","list: RenderInfoEntryBased[]"],"sources":["../src/handlers/finders.ts","../src/handlers/defs/parsers/songdotlink.ts","../src/handlers/defs/cache.ts","../src/handlers/defs/services/applemusic.ts","../src/handlers/defs/services/soundcloud.ts","../src/handlers/defs/services/spotify.ts","../src/handlers/core.ts"],"sourcesContent":["import type { Song } from \"structs/types\";\r\n\r\nimport { clean } from \"./common\";\r\nimport type { RenderSongInfo, SongParser, SongService } from \"./helpers\";\r\n\r\n// introducing... a pointer! resolves the circular dependency\r\nexport const $ = {\r\n\tservices: [] as SongService[],\r\n\tparsers: [] as SongParser[],\r\n};\r\n\r\nfunction sid(song: Song) {\r\n\treturn [song.service, song.type, song.id].join(\":\");\r\n}\r\n\r\n// parseLink -> parseCache, validateCache\r\n// rebuildLink -> linkCache, validateCache\r\n// renderSong -> renderCache, validateCache\r\n// validateSong -> validateCache\r\n\r\nconst parseCache = new Map<string, Song | null>();\r\nconst validateCache = new Map<string, boolean>();\r\n/**\r\n * Tries to parse the provided **link**. Returns a **Song** if successful, or `null` if nothing was found. Either response is temporarily cached.\r\n * @example ```ts\r\n * await parseLink(\"https://soundcloud.com/c0ncernn\");\r\n * // { service: \"soundcloud\", type: \"user\", id: \"914653456\" }\r\n * ```\r\n */\r\nexport async function parseLink(link: string): Promise<Song | null> {\r\n\tconst cleaned = clean(link);\r\n\tif (parseCache.has(cleaned)) return parseCache.get(cleaned)!;\r\n\r\n\tconst { hostname, pathname } = new URL(cleaned);\r\n\tconst path = pathname.slice(1).split(/\\/+/);\r\n\r\n\tlet song: Song | null = null;\r\n\tfor (const parser of $.parsers) {\r\n\t\tif (parser.hosts.includes(hostname)) {\r\n\t\t\tsong = await parser.parse(cleaned, hostname, path);\r\n\t\t\tif (song) break;\r\n\t\t}\r\n\t}\r\n\r\n\tparseCache.set(cleaned, song);\r\n\tif (song) validateCache.set(sid(song), true);\r\n\treturn song;\r\n}\r\n\r\nconst linkCache = new Map<string, string | null>();\r\n/**\r\n * Tries to recreate the link to the provided **Song**. Returns `string` if successful, or `null` if nothing was found. Either response is temporarily cached.\r\n * @example ```ts\r\n * await parseLink({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\r\n * // https://soundcloud.com/c0ncernn\r\n * ```\r\n */\r\nexport async function rebuildLink(song: Song): Promise<string | null> {\r\n\tconst id = sid(song);\r\n\tif (linkCache.has(id)) return linkCache.get(id)!;\r\n\r\n\tlet link = null;\r\n\tconst service = $.services.find(x => x.name === song.service);\r\n\tif (service?.types.includes(song.type)) link = await service.rebuild(song.type, song.id);\r\n\r\n\tlinkCache.set(id, link);\r\n\tif (link) validateCache.set(id, true);\r\n\treturn link;\r\n}\r\n\r\nconst renderCache = new Map<string, RenderSongInfo | null>();\r\n/**\r\n * Tries to render the provided **Song**. Returns `RenderSongInfo` if successful, or `null` if nothing was found. Either response is temporarily cached.\r\n * @example ```ts\r\n * await renderSong({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\r\n * // { label: \"leroy\", sublabel: \"Top tracks\", explicit: false, form: \"list\", ... }\r\n * ```\r\n */\r\nexport async function renderSong(song: Song): Promise<RenderSongInfo | null> {\r\n\tconst id = sid(song);\r\n\tif (renderCache.has(id)) return renderCache.get(id)!;\r\n\r\n\tlet info: RenderSongInfo | null = null;\r\n\tconst service = $.services.find(x => x.name === song.service);\r\n\tif (service?.types.includes(song.type)) info = await service.render(song.type, song.id);\r\n\r\n\trenderCache.set(id, info);\r\n\tif (song) validateCache.set(sid(song), true);\r\n\treturn info;\r\n}\r\n\r\n/**\r\n * Validates if the provided **Song** exists. Returns a `boolean` depending on if the check was successful or not. Either response is temporarily cached.\r\n * @example ```ts\r\n * await renderSong({ service: \"soundcloud\", type: \"user\", id: \"914653456\" });\r\n * // true\r\n * ```\r\n */\r\nexport async function validateSong(song: Song): Promise<boolean> {\r\n\tconst id = sid(song);\r\n\tif (validateCache.has(id)) return validateCache.get(id)!;\r\n\r\n\tlet valid = false;\r\n\tconst service = $.services.find(x => x.name === song.service);\r\n\tif (service?.types.includes(song.type)) valid = await service.validate(song.type, song.id);\r\n\r\n\tvalidateCache.set(id, valid);\r\n\treturn valid;\r\n}\r\n\r\n/** Clears the cache for all handler functions */\r\nexport function clearCache() {\r\n\tparseCache.clear();\r\n\tlinkCache.clear();\r\n\trenderCache.clear();\r\n\tvalidateCache.clear();\r\n}\r\n","import { parseNextData, request } from \"handlers/common\";\r\nimport { parseLink } from \"handlers/finders\";\r\nimport { type SongParser } from \"handlers/helpers\";\r\n\r\ninterface Next {\r\n\tprops: {\r\n\t\tpageProps: {\r\n\t\t\tpageData: {\r\n\t\t\t\tsections: {\r\n\t\t\t\t\tlinks: {\r\n\t\t\t\t\t\tplatform: string;\r\n\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t}[];\r\n\t\t\t\t}[];\r\n\t\t\t};\r\n\t\t};\r\n\t};\r\n}\r\n\r\nexport const songdotlink: SongParser = {\r\n\tname: \"song.link\",\r\n\thosts: [\r\n\t\t\"song.link\",\r\n\t\t\"album.link\",\r\n\t\t\"artist.link\",\r\n\t\t\"pods.link\",\r\n\t\t\"playlist.link\",\r\n\t\t\"mylink.page\",\r\n\t\t\"odesli.co\",\r\n\t],\r\n\tasync parse(link, _host, path) {\r\n\t\tconst [first, second, third] = path;\r\n\t\tif (!first || third) return null;\r\n\r\n\t\tif (second && Number.isNaN(+second)) return null;\r\n\t\telse if (\r\n\t\t\t!second && (!first.match(/^[A-z0-9-_]+$/) || first.match(/^[-_]/) || first.match(/[-_]$/))\r\n\t\t) return null;\r\n\r\n\t\tconst html = (await request({\r\n\t\t\turl: link,\r\n\t\t})).text;\r\n\r\n\t\tconst sections = parseNextData<Next>(html)?.props?.pageProps?.pageData?.sections;\r\n\t\tif (!sections) return null;\r\n\r\n\t\tconst links = sections.flatMap(x => x.links ?? []).filter(x => x.url && x.platform);\r\n\r\n\t\tconst valid = links.find(x => x.platform === \"spotify\")\r\n\t\t\t?? links.find(x => x.platform === \"soundcloud\")\r\n\t\t\t?? links.find(x => x.platform === \"appleMusic\");\r\n\t\tif (!valid) return null;\r\n\r\n\t\treturn await parseLink(valid.url);\r\n\t},\r\n};\r\n","const handlerCache = new Map<string, unknown>();\r\n\r\nexport function makeCache<T, Args>(name: string, retrieve: (...args: Args[]) => T) {\r\n\treturn {\r\n\t\tretrieve(...args: Args[]) {\r\n\t\t\tif (handlerCache.has(name)) return handlerCache.get(name) as T;\r\n\r\n\t\t\tconst res = retrieve(...args);\r\n\t\t\tif (res instanceof Promise) {\r\n\t\t\t\treturn res.then((ret: T) => {\r\n\t\t\t\t\thandlerCache.set(name, ret);\r\n\t\t\t\t\treturn ret;\r\n\t\t\t\t});\r\n\t\t\t} else {\r\n\t\t\t\thandlerCache.set(name, res);\r\n\t\t\t\treturn res;\r\n\t\t\t}\r\n\t\t},\r\n\t};\r\n}\r\n","import { PLAYLIST_LIMIT, request } from \"handlers/common\";\r\nimport type { RenderInfoBase, SongService } from \"handlers/helpers\";\r\n\r\nimport { makeCache } from \"../cache\";\r\n\r\ninterface APIDataEntry {\r\n\tattributes: {\r\n\t\turl: string;\r\n\t\tname: string;\r\n\t\tartistName?: string;\r\n\t\tcontentRating?: \"explicit\";\r\n\t\tdurationInMillis?: number;\r\n\t\tpreviews?: {\r\n\t\t\turl: string;\r\n\t\t}[];\r\n\t\tartwork?: {\r\n\t\t\turl: string;\r\n\t\t};\r\n\t};\r\n\trelationships: {\r\n\t\tsongs?: {\r\n\t\t\tdata: APIDataEntry[];\r\n\t\t};\r\n\t\ttracks?: {\r\n\t\t\tdata: APIDataEntry[];\r\n\t\t};\r\n\t};\r\n}\r\n\r\ninterface APIData {\r\n\tdata: [APIDataEntry];\r\n}\r\n\r\nconst geo = \"us\", defaultName = \"songspotlight\";\r\n\r\nconst applemusicToken = makeCache(\"applemusicToken\", async (html?: string) => {\r\n\thtml ??= (await request({\r\n\t\turl: `https://music.apple.com/${geo}/new`,\r\n\t})).text;\r\n\r\n\tconst asset = html.match(/src=\"(\\/assets\\/index~\\w+\\.js)\"/i)?.[1];\r\n\tif (!asset) return;\r\n\r\n\tconst js = (await request({\r\n\t\turl: `https://music.apple.com${asset}`,\r\n\t})).text;\r\n\r\n\tconst code = js.match(/\\w+=\"(ey.*?)\"/i)?.[1];\r\n\treturn code;\r\n});\r\n\r\nexport const applemusic: SongService = {\r\n\tname: \"applemusic\",\r\n\thosts: [\r\n\t\t\"music.apple.com\",\r\n\t\t\"geo.music.apple.com\",\r\n\t],\r\n\ttypes: [\"artist\", \"album\", \"playlist\", \"song\"],\r\n\tasync parse(_link, _host, path) {\r\n\t\tconst [country, type, name, id, fourth] = path;\r\n\t\tif (!country || !type || !this.types.includes(type) || !name || !id || fourth) return null;\r\n\r\n\t\tconst res = await request({\r\n\t\t\turl: this.rebuild(type, id) as string,\r\n\t\t});\r\n\t\tif (res.status !== 200) return null;\r\n\r\n\t\tawait applemusicToken.retrieve(res.text);\r\n\r\n\t\treturn {\r\n\t\t\tservice: this.name,\r\n\t\t\ttype,\r\n\t\t\tid,\r\n\t\t};\r\n\t},\r\n\tasync render(type, id) {\r\n\t\tconst token = await applemusicToken.retrieve();\r\n\t\tif (!token) return null;\r\n\r\n\t\tconst res = await request({\r\n\t\t\turl: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s/${id}?include=songs`,\r\n\t\t\theaders: {\r\n\t\t\t\tauthorization: `Bearer ${token}`,\r\n\t\t\t\torigin: \"https://music.apple.com\",\r\n\t\t\t},\r\n\t\t});\r\n\t\tif (res.status !== 200) return null;\r\n\r\n\t\tconst { attributes, relationships } = (res.json as APIData).data[0];\r\n\r\n\t\tconst base: RenderInfoBase = {\r\n\t\t\tlabel: attributes.name,\r\n\t\t\tsublabel: attributes.artistName ?? \"Top Songs\",\r\n\t\t\texplicit: attributes.contentRating === \"explicit\",\r\n\t\t};\r\n\t\tconst thumbnailUrl = attributes.artwork?.url?.replace(/{[wh]}/g, \"128\");\r\n\r\n\t\tif (type === \"song\") {\r\n\t\t\tconst duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"single\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tsingle: {\r\n\t\t\t\t\taudio: previewUrl && duration\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tpreviewUrl,\r\n\t\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink: attributes.url,\r\n\t\t\t\t},\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tconst songs = (relationships.songs ?? relationships.tracks)?.data;\r\n\t\tif (!songs) return null;\r\n\r\n\t\treturn {\r\n\t\t\t...base,\r\n\t\t\tform: \"list\",\r\n\t\t\tthumbnailUrl,\r\n\t\t\tlist: songs.slice(0, PLAYLIST_LIMIT).map(({ attributes: song }) => {\r\n\t\t\t\tconst duration = song.durationInMillis, previewUrl = song.previews?.[0]?.url;\r\n\t\t\t\treturn {\r\n\t\t\t\t\tlabel: song.name,\r\n\t\t\t\t\tsublabel: song.artistName!,\r\n\t\t\t\t\texplicit: song.contentRating === \"explicit\",\r\n\t\t\t\t\taudio: previewUrl && duration\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tpreviewUrl,\r\n\t\t\t\t\t\t\tduration,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink: song.url,\r\n\t\t\t\t};\r\n\t\t\t}),\r\n\t\t};\r\n\t},\r\n\tasync validate(type, id) {\r\n\t\treturn (await request({\r\n\t\t\turl: this.rebuild(type, id) as string,\r\n\t\t})).status === 200;\r\n\t},\r\n\trebuild(type, id) {\r\n\t\treturn `https://music.apple.com/${geo}/${type}/${defaultName}/${id}`;\r\n\t},\r\n};\r\n","import { PLAYLIST_LIMIT, request } from \"handlers/common\";\r\nimport { parseLink } from \"handlers/finders\";\r\nimport { type RenderInfoBase, type RenderInfoEntryBased, type SongService } from \"handlers/helpers\";\r\n\r\ninterface oEmbedData {\r\n\thtml: string;\r\n}\r\n\r\ninterface Transcoding {\r\n\tduration: number;\r\n\turl: string;\r\n\tformat: {\r\n\t\tprotocol: string;\r\n\t};\r\n}\r\n\r\ninterface WidgetData {\r\n\tartwork_url: string;\r\n\tavatar_url?: string;\r\n\ttitle: string;\r\n\tid: number;\r\n\tpermalink_url: string;\r\n\tusername?: string;\r\n\tuser?: {\r\n\t\tusername: string;\r\n\t};\r\n\tpublisher_metadata?: {\r\n\t\texplicit: boolean;\r\n\t};\r\n\tmedia?: {\r\n\t\ttranscodings: Transcoding[];\r\n\t};\r\n\ttracks?: WidgetData[];\r\n}\r\n\r\ninterface TracksWidgetData {\r\n\tcollection: WidgetData[];\r\n}\r\n\r\ninterface PreviewResponse {\r\n\turl: string;\r\n}\r\n\r\nconst client_id = \"nIjtjiYnjkOhMyh5xrbqEW12DxeJVnic\";\r\n\r\nfunction parseWidget(type: string, id: string, tracks: true): Promise<TracksWidgetData | undefined>;\r\nfunction parseWidget(type: string, id: string, tracks: false): Promise<WidgetData | undefined>;\r\nasync function parseWidget(type: string, id: string, tracks: boolean) {\r\n\treturn (await request({\r\n\t\turl: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? \"/tracks?limit=20\" : \"\"}`,\r\n\t\tquery: {\r\n\t\t\tclient_id,\r\n\t\t\t// app version isnt static but lets hope soundcloud doesnt mind :) :) :)\r\n\t\t\tapp_version: \"1764154491\",\r\n\t\t\tformat: \"json\",\r\n\t\t\trepresentation: \"full\",\r\n\t\t},\r\n\t})).json;\r\n}\r\nasync function parsePreview(transcodings: Transcoding[]) {\r\n\tconst preview = transcodings.sort((a, b) => {\r\n\t\tconst isA = a.format.protocol === \"progressive\";\r\n\t\tconst isB = b.format.protocol === \"progressive\";\r\n\r\n\t\treturn (isA && !isB) ? -1 : (isB && !isA) ? 1 : 0;\r\n\t})?.[0];\r\n\r\n\tif (preview?.url && preview?.duration) {\r\n\t\tconst link = (await request({\r\n\t\t\turl: preview.url,\r\n\t\t\tquery: {\r\n\t\t\t\tclient_id,\r\n\t\t\t},\r\n\t\t}))\r\n\t\t\t.json as PreviewResponse;\r\n\t\tif (!link?.url) return;\r\n\r\n\t\treturn {\r\n\t\t\tduration: preview.duration,\r\n\t\t\tpreviewUrl: link.url,\r\n\t\t};\r\n\t}\r\n}\r\n\r\nexport const soundcloud: SongService = {\r\n\tname: \"soundcloud\",\r\n\thosts: [\r\n\t\t\"soundcloud.com\",\r\n\t\t\"m.soundcloud.com\",\r\n\t\t\"on.soundcloud.com\",\r\n\t],\r\n\ttypes: [\"user\", \"track\", \"playlist\"],\r\n\tasync parse(link, host, path) {\r\n\t\tif (host === \"on.soundcloud.com\") {\r\n\t\t\tif (!path[0] || path[1]) return null;\r\n\t\t\tconst { url, status } = await request({\r\n\t\t\t\turl: link,\r\n\t\t\t});\r\n\t\t\treturn status === 200 ? await parseLink(url) : null;\r\n\t\t} else {\r\n\t\t\tconst [user, second, track, fourth] = path;\r\n\r\n\t\t\tlet valid = false;\r\n\t\t\tif (user && !second) valid = true; // user\r\n\t\t\telse if (user && second && second !== \"sets\" && !track) valid = true; // playlist\r\n\t\t\telse if (user && second === \"sets\" && track && !fourth) valid = true; // track\r\n\r\n\t\t\tif (!valid) return null;\r\n\r\n\t\t\tconst data = (await request({\r\n\t\t\t\turl: \"https://soundcloud.com/oembed\",\r\n\t\t\t\tquery: {\r\n\t\t\t\t\tformat: \"json\",\r\n\t\t\t\t\turl: link,\r\n\t\t\t\t},\r\n\t\t\t})).json as oEmbedData;\r\n\t\t\tif (!data?.html) return null;\r\n\r\n\t\t\t// https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F1053322828&show_artwork=true\r\n\t\t\tconst rawUrl = data.html.match(/w\\.soundcloud\\.com.*?url=(.*?)[&\"]/)?.[1];\r\n\t\t\tif (!rawUrl) return null;\r\n\r\n\t\t\t// https://api.soundcloud.com/tracks/1053322828\r\n\t\t\tconst splits = decodeURIComponent(rawUrl).split(/\\/+/);\r\n\t\t\tconst kind = splits[2], id = splits[3];\r\n\t\t\tif (!kind || !id) return null;\r\n\r\n\t\t\treturn {\r\n\t\t\t\tservice: this.name,\r\n\t\t\t\ttype: kind.slice(0, -1), // turns tracks -> track\r\n\t\t\t\tid,\r\n\t\t\t};\r\n\t\t}\r\n\t},\r\n\tasync render(type, id) {\r\n\t\tconst data = await parseWidget(type, id, false);\r\n\t\tif (!data?.id) return null;\r\n\r\n\t\tconst base: RenderInfoBase = {\r\n\t\t\tlabel: data.title ?? data.username,\r\n\t\t\tsublabel: data.user?.username ?? \"Top tracks\",\r\n\t\t\texplicit: Boolean(data.publisher_metadata?.explicit),\r\n\t\t};\r\n\t\tconst thumbnailUrl = data.artwork_url ?? data.avatar_url;\r\n\r\n\t\tif (type === \"track\") {\r\n\t\t\tconst audio = await parsePreview(data.media?.transcodings ?? []);\r\n\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"single\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tsingle: {\r\n\t\t\t\t\taudio,\r\n\t\t\t\t\tlink: data.permalink_url,\r\n\t\t\t\t},\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tlet tracks: WidgetData[] = [];\r\n\t\tif (type === \"user\") {\r\n\t\t\tconst got = await parseWidget(type, id, true);\r\n\t\t\tif (!got?.collection) return null;\r\n\r\n\t\t\ttracks = got.collection;\r\n\t\t} else {\r\n\t\t\tif (!data.tracks) return null;\r\n\r\n\t\t\ttracks = data.tracks;\r\n\t\t}\r\n\r\n\t\tconst list: RenderInfoEntryBased[] = [];\r\n\t\tfor (const track of tracks) {\r\n\t\t\t// unavailable songs\r\n\t\t\tif (!track.title || list.length >= PLAYLIST_LIMIT) continue;\r\n\r\n\t\t\tconst audio = await parsePreview(track.media?.transcodings ?? []);\r\n\r\n\t\t\tlist.push({\r\n\t\t\t\tlabel: track.title,\r\n\t\t\t\tsublabel: track.user?.username ?? \"IDKK\",\r\n\t\t\t\texplicit: Boolean(track.publisher_metadata!.explicit),\r\n\t\t\t\taudio,\r\n\t\t\t\tlink: track.permalink_url,\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\treturn {\r\n\t\t\t...base,\r\n\t\t\tform: \"list\",\r\n\t\t\tthumbnailUrl,\r\n\t\t\tlist,\r\n\t\t};\r\n\t},\r\n\tasync validate(type, id) {\r\n\t\treturn (await parseWidget(type, id, false))?.id !== undefined;\r\n\t},\r\n\tasync rebuild(type, id) {\r\n\t\treturn (await parseWidget(type, id, false))?.permalink_url ?? null;\r\n\t},\r\n};\r\n","import { parseNextData, PLAYLIST_LIMIT, request } from \"handlers/common\";\r\nimport { type RenderInfoBase, type RenderInfoEntryBased, type SongService } from \"handlers/helpers\";\r\n\r\ninterface Next {\r\n\tprops: {\r\n\t\tpageProps: {\r\n\t\t\ttitle?: string;\r\n\t\t\tstate?: {\r\n\t\t\t\tdata: {\r\n\t\t\t\t\tentity: {\r\n\t\t\t\t\t\turi: string;\r\n\t\t\t\t\t\ttitle: string;\r\n\t\t\t\t\t\tsubtitle: string;\r\n\t\t\t\t\t\tisExplicit: boolean;\r\n\t\t\t\t\t\tartists?: {\r\n\t\t\t\t\t\t\tname: string;\r\n\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\tduration?: number;\r\n\t\t\t\t\t\taudioPreview?: {\r\n\t\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t\ttrackList?: {\r\n\t\t\t\t\t\t\turi: string;\r\n\t\t\t\t\t\t\ttitle: string;\r\n\t\t\t\t\t\t\tsubtitle: string;\r\n\t\t\t\t\t\t\tisExplicit: boolean;\r\n\t\t\t\t\t\t\tartists?: {\r\n\t\t\t\t\t\t\t\tname: string;\r\n\t\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\t\tduration?: number;\r\n\t\t\t\t\t\t\taudioPreview?: {\r\n\t\t\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t\t\t};\r\n\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\tvisualIdentity: {\r\n\t\t\t\t\t\t\timage: {\r\n\t\t\t\t\t\t\t\turl: string;\r\n\t\t\t\t\t\t\t\tmaxWidth: number;\r\n\t\t\t\t\t\t\t}[];\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t};\r\n\t\t\t\t};\r\n\t\t\t};\r\n\t\t};\r\n\t};\r\n}\r\n\r\nasync function parseEmbed(type: string, id: string) {\r\n\treturn parseNextData<Next>(\r\n\t\t(await request({\r\n\t\t\turl: `https://open.spotify.com/embed/${type}/${id}`,\r\n\t\t})).text,\r\n\t);\r\n}\r\n\r\nfunction fromUri(uri: string) {\r\n\tconst [sanityCheck, type, id] = uri.split(\":\");\r\n\tif (sanityCheck === \"spotify\" && type && id) return `https://open.spotify.com/${type}/${id}`;\r\n\telse return null;\r\n}\r\n\r\nexport const spotify: SongService = {\r\n\tname: \"spotify\",\r\n\thosts: [\r\n\t\t\"open.spotify.com\",\r\n\t],\r\n\ttypes: [\"track\", \"album\", \"playlist\", \"artist\"],\r\n\tasync parse(_link, _host, path) {\r\n\t\tconst [type, id, third] = path;\r\n\t\tif (!type || !this.types.includes(type as never) || !id || third) return null;\r\n\r\n\t\tif (!await this.validate(type, id)) return null;\r\n\r\n\t\treturn {\r\n\t\t\tservice: this.name,\r\n\t\t\ttype,\r\n\t\t\tid,\r\n\t\t};\r\n\t},\r\n\tasync render(type, id) {\r\n\t\tconst data = (await parseEmbed(type, id) as Next)?.props?.pageProps?.state?.data?.entity;\r\n\t\tif (!data) return null;\r\n\r\n\t\tconst base: RenderInfoBase = {\r\n\t\t\tlabel: data.title,\r\n\t\t\tsublabel: data.subtitle ?? data.artists?.map(x => x.name).join(\", \"),\r\n\t\t\texplicit: Boolean(data.isExplicit),\r\n\t\t};\r\n\t\tconst thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]?.url;\r\n\r\n\t\tif (type === \"track\") {\r\n\t\t\tconst link = fromUri(data.uri)!;\r\n\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"single\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tsingle: {\r\n\t\t\t\t\taudio: (data.audioPreview && data.duration)\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tduration: data.duration,\r\n\t\t\t\t\t\t\tpreviewUrl: data.audioPreview.url,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink,\r\n\t\t\t\t},\r\n\t\t\t};\r\n\t\t} else {\r\n\t\t\tconst list: RenderInfoEntryBased[] = [];\r\n\t\t\tfor (const track of (data.trackList ?? [])) {\r\n\t\t\t\tif (list.length >= PLAYLIST_LIMIT) continue;\r\n\t\t\t\tconst link = fromUri(track.uri)!;\r\n\r\n\t\t\t\tlist.push({\r\n\t\t\t\t\tlabel: track.title,\r\n\t\t\t\t\tsublabel: track.subtitle ?? track.artists?.map(x => x.name).join(\", \"),\r\n\t\t\t\t\texplicit: Boolean(track.isExplicit),\r\n\t\t\t\t\taudio: (track.audioPreview && track.duration)\r\n\t\t\t\t\t\t? {\r\n\t\t\t\t\t\t\tduration: track.duration,\r\n\t\t\t\t\t\t\tpreviewUrl: track.audioPreview.url,\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t: undefined,\r\n\t\t\t\t\tlink,\r\n\t\t\t\t});\r\n\t\t\t}\r\n\r\n\t\t\treturn {\r\n\t\t\t\t...base,\r\n\t\t\t\tform: \"list\",\r\n\t\t\t\tthumbnailUrl,\r\n\t\t\t\tlist,\r\n\t\t\t};\r\n\t\t}\r\n\t},\r\n\tasync validate(type, id) {\r\n\t\treturn !(await parseEmbed(type, id))?.props?.pageProps?.title;\r\n\t},\r\n\trebuild(type, id) {\r\n\t\treturn `https://open.spotify.com/${type}/${id}`;\r\n\t},\r\n};\r\n","import { songdotlink } from \"./defs/parsers/songdotlink\";\r\nimport { applemusic } from \"./defs/services/applemusic\";\r\nimport { soundcloud } from \"./defs/services/soundcloud\";\r\nimport { spotify } from \"./defs/services/spotify\";\r\nimport { $ } from \"./finders\";\r\nimport type { SongParser, SongService } from \"./helpers\";\r\n\r\nexport const services = [\r\n\tspotify,\r\n\tsoundcloud,\r\n\tapplemusic,\r\n] as SongService[];\r\n$.services = services;\r\n\r\nexport const parsers = [\r\n\tsongdotlink,\r\n\t...services,\r\n] as SongParser[];\r\n$.parsers = parsers;\r\n"],"mappings":"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"}
|
|
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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"}
|
package/dist/handlers.d.ts
CHANGED
package/dist/handlers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./common-
|
|
2
|
-
import { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong } from "./core-
|
|
1
|
+
import "./common-DrSlxvDY.js";
|
|
2
|
+
import { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong } from "./core-XI62QUiR.js";
|
|
3
3
|
|
|
4
4
|
export { $, clearCache, parseLink, parsers, rebuildLink, renderSong, services, validateSong };
|
package/dist/structs.js
CHANGED
package/dist/util.js
CHANGED
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
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": "1.3.2",
|
|
4
|
+
"description": "Song Spotlight API types and song validation module",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "rm -rf dist && rolldown -c rolldown.config.mts",
|
|
8
|
+
"prepublishOnly": "bun run build"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./handlers": {
|
|
12
|
+
"types": "./dist/handlers.d.ts",
|
|
13
|
+
"default": "./dist/handlers.js"
|
|
14
|
+
},
|
|
15
|
+
"./structs": {
|
|
16
|
+
"types": "./dist/structs.d.ts",
|
|
17
|
+
"default": "./dist/structs.js"
|
|
18
|
+
},
|
|
19
|
+
"./util": {
|
|
20
|
+
"types": "./dist/util.d.ts",
|
|
21
|
+
"default": "./dist/util.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"homepage": "https://github.com/nexpid-labs/SongSpotlight/tree/main/packages/api",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/nexpid-labs/SongSpotlight.git",
|
|
35
|
+
"directory": "packages/api"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/nexpid-labs/SongSpotlight/issues"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/bun": "latest",
|
|
42
|
+
"rolldown": "^1.0.0-beta.34",
|
|
43
|
+
"rolldown-plugin-dts": "^0.15.10"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"typescript": "^5"
|
|
47
|
+
},
|
|
48
|
+
"optionalDependencies": {
|
|
49
|
+
"zod": "^4.3.6"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"common-PYa2ZOr8.js","names":["json: unknown"],"sources":["../package.json","../src/handlers/common.ts"],"sourcesContent":["{\n\t\"name\": \"@song-spotlight/api\",\n\t\"version\": \"1.3.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\t\"./util\": {\n\t\t\t\"types\": \"./dist/util.d.ts\",\n\t\t\t\"default\": \"./dist/util.js\"\n\t\t}\n\t},\n\t\"publishConfig\": {\n\t\t\"access\": \"public\"\n\t},\n\t\"files\": [\n\t\t\"dist\"\n\t],\n\t\"license\": \"MIT\",\n\t\"homepage\": \"https://github.com/nexpid-labs/SongSpotlight/tree/main/packages/api\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/nexpid-labs/SongSpotlight.git\",\n\t\t\"directory\": \"packages/api\"\n\t},\n\t\"bugs\": {\n\t\t\"url\": \"https://github.com/nexpid-labs/SongSpotlight/issues\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@types/bun\": \"latest\",\n\t\t\"rolldown\": \"^1.0.0-beta.34\",\n\t\t\"rolldown-plugin-dts\": \"^0.15.10\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"typescript\": \"^5\"\n\t},\n\t\"optionalDependencies\": {\n\t\t\"zod\": \"^4.3.6\"\n\t}\n}\n","import { version } from \"@\";\r\n\r\ninterface RequestOptions {\r\n\turl: string;\r\n\tmethod?: string;\r\n\tquery?: Record<string, string>;\r\n\theaders?: Record<string, string>;\r\n\tbody?: unknown;\r\n}\r\n\r\nexport function clean(link: string) {\r\n\tconst url = new URL(link);\r\n\turl.protocol = \"https\";\r\n\turl.username =\r\n\t\turl.password =\r\n\t\turl.port =\r\n\t\turl.search =\r\n\t\turl.hash =\r\n\t\t\t\"\";\r\n\r\n\treturn url.toString().replace(/\\/?$/, \"\");\r\n}\r\n\r\nlet makeRequest = fetch;\r\n/**\r\n * Lets you to set a custom `fetch()` function. Useful for passing requests through Electron's [net.fetch](https://www.electronjs.org/docs/latest/api/net#netfetchinput-init) for example.\r\n * @example ```ts\r\n * import { net } from \"electron\";\r\n *\r\n * setFetchHandler(net.fetch as unknown as typeof fetch);\r\n * ```\r\n */\r\nexport function setFetchHandler(fetcher: typeof fetch) {\r\n\tmakeRequest = fetcher;\r\n}\r\n\r\nexport async function request(options: RequestOptions) {\r\n\tif (options.body) {\r\n\t\tconst body = JSON.stringify(options.body);\r\n\t\toptions.body = body;\r\n\r\n\t\toptions.headers ??= {};\r\n\t\toptions.headers[\"content-type\"] ??= \"application/json\";\r\n\t\toptions.headers[\"content-length\"] ??= String(body.length);\r\n\t}\r\n\r\n\tconst url = new URL(options.url);\r\n\tfor (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);\r\n\r\n\tconst res = await makeRequest(url, {\r\n\t\tmethod: options.method,\r\n\t\tredirect: \"follow\",\r\n\t\theaders: {\r\n\t\t\t\"accept\": \"*/*\",\r\n\t\t\t\"user-agent\": `SongSpotlight/${version}`,\r\n\t\t\t\"cache-control\": \"public, max-age=3600\",\r\n\t\t\t...(options.headers ?? {}),\r\n\t\t},\r\n\t\t// @ts-expect-error Untyped cloudflare workers cache type\r\n\t\tcf: {\r\n\t\t\tcacheTtl: 3600,\r\n\t\t\tcacheEverything: true,\r\n\t\t},\r\n\t\tbody: options.body as never,\r\n\t});\r\n\r\n\tconst text = await res.text();\r\n\tlet json: unknown;\r\n\ttry {\r\n\t\tjson = JSON.parse(text);\r\n\t} catch {\r\n\t\tjson = null;\r\n\t}\r\n\r\n\treturn {\r\n\t\tok: res.ok,\r\n\t\tredirected: res.redirected,\r\n\t\turl: res.url,\r\n\t\tstatus: res.status,\r\n\t\theaders: res.headers,\r\n\t\ttext,\r\n\t\tjson,\r\n\t};\r\n}\r\n\r\nexport function parseNextData<Type>(html: string) {\r\n\tconst data = html.match(/id=\"__NEXT_DATA__\" type=\"application\\/json\">(.*?)<\\/script>/)?.[1];\r\n\tif (!data) return undefined;\r\n\r\n\ttry {\r\n\t\treturn JSON.parse(data) as Type;\r\n\t} catch {\r\n\t\treturn undefined;\r\n\t}\r\n}\r\n\r\n// limit of 15 songs per playlist\r\nexport const PLAYLIST_LIMIT = 15;\r\n"],"mappings":";cAEY;;;;ACQZ,SAAgB,MAAM,MAAc;CACnC,MAAM,MAAM,IAAI,IAAI;AACpB,KAAI,WAAW;AACf,KAAI,WACH,IAAI,WACJ,IAAI,OACJ,IAAI,SACJ,IAAI,OACH;AAEF,QAAO,IAAI,WAAW,QAAQ,QAAQ;;AAGvC,IAAI,cAAc;;;;;;;;;AASlB,SAAgB,gBAAgB,SAAuB;AACtD,eAAc;;AAGf,eAAsB,QAAQ,SAAyB;AACtD,KAAI,QAAQ,MAAM;EACjB,MAAM,OAAO,KAAK,UAAU,QAAQ;AACpC,UAAQ,OAAO;AAEf,UAAQ,YAAY;AACpB,UAAQ,QAAQ,oBAAoB;AACpC,UAAQ,QAAQ,sBAAsB,OAAO,KAAK;;CAGnD,MAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,SAAS,IAAK,KAAI,aAAa,IAAI,KAAK;CAE1F,MAAM,MAAM,MAAM,YAAY,KAAK;EAClC,QAAQ,QAAQ;EAChB,UAAU;EACV,SAAS;GACR,UAAU;GACV,cAAc,iBAAiB;GAC/B,iBAAiB;GACjB,GAAI,QAAQ,WAAW;;EAGxB,IAAI;GACH,UAAU;GACV,iBAAiB;;EAElB,MAAM,QAAQ;;CAGf,MAAM,OAAO,MAAM,IAAI;CACvB,IAAIA;AACJ,KAAI;AACH,SAAO,KAAK,MAAM;SACX;AACP,SAAO;;AAGR,QAAO;EACN,IAAI,IAAI;EACR,YAAY,IAAI;EAChB,KAAK,IAAI;EACT,QAAQ,IAAI;EACZ,SAAS,IAAI;EACb;EACA;;;AAIF,SAAgB,cAAoB,MAAc;CACjD,MAAM,OAAO,KAAK,MAAM,iEAAiE;AACzF,KAAI,CAAC,KAAM,QAAO;AAElB,KAAI;AACH,SAAO,KAAK,MAAM;SACX;AACP,SAAO;;;AAKT,MAAa,iBAAiB"}
|