@song-spotlight/api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 nexpid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @song-spotlight/api
2
+
3
+ [Song Spotlight](https://github.com/nexpid-labs/SongSpotlight) API types and song validation module
4
+
5
+ ## How to use
6
+
7
+ TODO!!!
8
+
9
+ ## Development
10
+
11
+ To install dependencies:
12
+
13
+ ```bash
14
+ bun install
15
+ ```
16
+
17
+ To build:
18
+
19
+ ```bash
20
+ bun run build
21
+ ```
22
+
23
+ To publish:
24
+
25
+ ```bash
26
+ bun run publish
27
+ ```
28
+
29
+ This project was created using `bun init` in bun v1.2.20. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
@@ -0,0 +1,480 @@
1
+ //#region package.json
2
+ var version = "0.1.0";
3
+
4
+ //#endregion
5
+ //#region src/handlers/common.ts
6
+ function clean(link) {
7
+ const url = new URL(link);
8
+ url.protocol = "https";
9
+ url.username = url.password = url.port = url.search = url.hash = "";
10
+ return url.toString().replace(/\/?$/, "/");
11
+ }
12
+ async function request(options) {
13
+ if (options.body) {
14
+ const body = JSON.stringify(options.body);
15
+ options.body = body;
16
+ options.headers ??= {};
17
+ options.headers["content-type"] ??= "application/json";
18
+ options.headers["content-length"] ??= String(body.length);
19
+ }
20
+ const url = new URL(options.url);
21
+ for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
22
+ const res = await fetch(url, {
23
+ method: options.method,
24
+ cache: "force-cache",
25
+ redirect: "follow",
26
+ headers: {
27
+ "accept": "*/*",
28
+ "user-agent": `song-spotlight/v${version}`,
29
+ "cache-control": "public, max-age=3600",
30
+ ...options.headers
31
+ },
32
+ body: options.body
33
+ });
34
+ const text = await res.text();
35
+ let json;
36
+ try {
37
+ json = JSON.parse(text);
38
+ } catch {
39
+ json = null;
40
+ }
41
+ return {
42
+ ok: res.ok,
43
+ redirected: res.redirected,
44
+ url: res.url,
45
+ status: res.status,
46
+ headers: res.headers,
47
+ text,
48
+ json
49
+ };
50
+ }
51
+ function parseNextData(html) {
52
+ const data = html.match(/id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/)?.[1];
53
+ if (!data) return void 0;
54
+ try {
55
+ return JSON.parse(data);
56
+ } catch {
57
+ return void 0;
58
+ }
59
+ }
60
+ const PLAYLIST_LIMIT = 15;
61
+
62
+ //#endregion
63
+ //#region src/handlers/finders.ts
64
+ const $ = {
65
+ services: [],
66
+ parsers: []
67
+ };
68
+ function sid(song) {
69
+ return [
70
+ song.service,
71
+ song.type,
72
+ song.id
73
+ ].join(":");
74
+ }
75
+ const parseCache = /* @__PURE__ */ new Map();
76
+ const linkCache = /* @__PURE__ */ new Map();
77
+ async function parseLink(link) {
78
+ const cleaned = clean(link);
79
+ if (parseCache.has(cleaned)) return parseCache.get(cleaned);
80
+ const { hostname, pathname } = new URL(cleaned);
81
+ const path = pathname.slice(1).split(/\/+/);
82
+ let song = null;
83
+ for (const parser of $.parsers) if (parser.hosts.includes(hostname)) {
84
+ song = await parser.parse(cleaned, hostname, path);
85
+ if (song) break;
86
+ }
87
+ parseCache.set(cleaned, song);
88
+ if (song) linkCache.set(sid(song), cleaned);
89
+ return song;
90
+ }
91
+ const renderCache = /* @__PURE__ */ new Map();
92
+ async function renderSong(song) {
93
+ const id = sid(song);
94
+ if (renderCache.has(id)) return renderCache.get(id);
95
+ let info = null;
96
+ const service = $.services.find((x) => x.name === song.service);
97
+ if (service?.types.includes(song.type)) info = await service.render(song.type, song.id);
98
+ renderCache.set(id, info);
99
+ return info;
100
+ }
101
+ async function toLink(song) {
102
+ const id = sid(song);
103
+ if (linkCache.has(id)) return linkCache.get(id);
104
+ let link = null;
105
+ const service = $.services.find((x) => x.name === song.service);
106
+ if (service?.types.includes(song.type)) link = await service.from(song.type, song.id);
107
+ const cleaned = link ? clean(link) : link;
108
+ linkCache.set(id, cleaned);
109
+ if (cleaned) parseCache.set(cleaned, {
110
+ service: song.service,
111
+ type: song.type,
112
+ id: song.id
113
+ });
114
+ return link;
115
+ }
116
+
117
+ //#endregion
118
+ //#region src/handlers/defs/parsers/songdotlink.ts
119
+ const songdotlink = {
120
+ hosts: [
121
+ "song.link",
122
+ "album.link",
123
+ "artist.link",
124
+ "pods.link",
125
+ "playlist.link",
126
+ "mylink.page",
127
+ "odesli.co"
128
+ ],
129
+ async parse(link, _host, path) {
130
+ const [first, second, third] = path;
131
+ if (!first || third) return null;
132
+ if (second && Number.isNaN(+second)) return null;
133
+ else if (!second && (!first.match(/^[A-z0-9-_]+$/) || first.match(/^[-_]/) || first.match(/[-_]$/))) return null;
134
+ const html = (await request({ url: link })).text;
135
+ const sections = parseNextData(html)?.props?.pageProps?.pageData?.sections;
136
+ if (!sections) return null;
137
+ const links = sections.flatMap((x) => x.links ?? []).filter((x) => x.url && x.platform);
138
+ const valid = links.find((x) => x.platform === "spotify") ?? links.find((x) => x.platform === "soundcloud") ?? links.find((x) => x.platform === "appleMusic");
139
+ if (!valid) return null;
140
+ return await parseLink(valid.url);
141
+ }
142
+ };
143
+
144
+ //#endregion
145
+ //#region src/handlers/defs/cache.ts
146
+ const handlerCache = /* @__PURE__ */ new Map();
147
+ function makeCache(name, retrieve) {
148
+ return { retrieve(...args) {
149
+ if (handlerCache.has(name)) return handlerCache.get(name);
150
+ const res = retrieve(...args);
151
+ if (res instanceof Promise) return res.then((ret) => {
152
+ handlerCache.set(name, ret);
153
+ return ret;
154
+ });
155
+ else {
156
+ handlerCache.set(name, res);
157
+ return res;
158
+ }
159
+ } };
160
+ }
161
+
162
+ //#endregion
163
+ //#region src/handlers/defs/services/applemusic.ts
164
+ const geo = "fr", defaultName = "songspotlight";
165
+ const applemusicToken = makeCache("applemusicToken", async (html) => {
166
+ html ??= (await request({ url: `https://music.apple.com/${geo}/new` })).text;
167
+ const asset = html.match(/src="(\/assets\/index-\w+\.js)"/i)?.[1];
168
+ if (!asset) return;
169
+ const js = (await request({ url: `https://music.apple.com${asset}` })).text;
170
+ const code = js.match(/\w+="(ey.*?)"/i)?.[1];
171
+ return code;
172
+ });
173
+ const applemusic = {
174
+ name: "applemusic",
175
+ hosts: ["music.apple.com", "geo.music.apple.com"],
176
+ types: [
177
+ "artist",
178
+ "album",
179
+ "playlist",
180
+ "song"
181
+ ],
182
+ async parse(_link, _host, path) {
183
+ const [country, type, name, id, fourth] = path;
184
+ if (!country || !type || !this.types.includes(type) || !name || !id || fourth) return null;
185
+ const res = await request({ url: `https://music.apple.com/${geo}/${type}/${defaultName}/${id}` });
186
+ if (res.status !== 200) return null;
187
+ await applemusicToken.retrieve(res.text);
188
+ return {
189
+ service: this.name,
190
+ type,
191
+ id
192
+ };
193
+ },
194
+ async render(type, id) {
195
+ const token = await applemusicToken.retrieve();
196
+ if (!token) return null;
197
+ const res = await request({
198
+ url: `https://amp-api.music.apple.com/v1/catalog/${geo}/${type}s/${id}?include=songs`,
199
+ headers: {
200
+ authorization: `Bearer ${token}`,
201
+ origin: "https://music.apple.com"
202
+ }
203
+ });
204
+ if (res.status !== 200) return null;
205
+ const { attributes, relationships } = res.json.data[0];
206
+ const base = {
207
+ label: attributes.name,
208
+ sublabel: attributes.artistName ?? "Top Songs",
209
+ explicit: attributes.contentRating === "explicit"
210
+ };
211
+ const thumbnailUrl = attributes.artwork?.url?.replace("{w}", "128")?.replace("{h}", "128");
212
+ if (type === "song") {
213
+ const duration = attributes.durationInMillis, previewUrl = attributes.previews?.[0]?.url;
214
+ return {
215
+ ...base,
216
+ form: "single",
217
+ thumbnailUrl,
218
+ single: {
219
+ audio: previewUrl && duration ? {
220
+ previewUrl,
221
+ duration
222
+ } : void 0,
223
+ link: attributes.url
224
+ }
225
+ };
226
+ }
227
+ const songs = (relationships.songs ?? relationships.tracks)?.data;
228
+ if (!songs) return null;
229
+ return {
230
+ ...base,
231
+ form: "list",
232
+ thumbnailUrl,
233
+ list: songs.slice(0, PLAYLIST_LIMIT).map(({ attributes: song }) => {
234
+ const duration = song.durationInMillis, previewUrl = song.previews?.[0]?.url;
235
+ return {
236
+ label: song.name,
237
+ sublabel: song.artistName,
238
+ explicit: song.contentRating === "explicit",
239
+ audio: previewUrl && duration ? {
240
+ previewUrl,
241
+ duration
242
+ } : void 0,
243
+ link: song.url
244
+ };
245
+ })
246
+ };
247
+ },
248
+ from(type, id) {
249
+ return `https://music.apple.com/us/${type}/${defaultName}/${id}`;
250
+ }
251
+ };
252
+
253
+ //#endregion
254
+ //#region src/handlers/defs/services/soundcloud.ts
255
+ const client_id = "nIjtjiYnjkOhMyh5xrbqEW12DxeJVnic";
256
+ async function parseWidget(type, id, tracks) {
257
+ return (await request({
258
+ url: `https://api-widget.soundcloud.com/${type}s/${id}${tracks ? "/tracks?limit=20" : ""}`,
259
+ query: {
260
+ client_id,
261
+ app_version: "1752674865",
262
+ format: "json",
263
+ representation: "full"
264
+ }
265
+ })).json;
266
+ }
267
+ async function parsePreview(transcodings) {
268
+ const preview = transcodings.sort((a, b) => {
269
+ const isA = a.format.protocol === "progressive";
270
+ const isB = b.format.protocol === "progressive";
271
+ return isA && !isB ? -1 : isB && !isA ? 1 : 0;
272
+ })?.[0];
273
+ if (preview?.url && preview?.duration) {
274
+ const link = (await request({
275
+ url: preview.url,
276
+ query: { client_id }
277
+ })).json;
278
+ if (!link?.url) return;
279
+ return {
280
+ duration: preview.duration,
281
+ previewUrl: link.url
282
+ };
283
+ }
284
+ }
285
+ const soundcloud = {
286
+ name: "soundcloud",
287
+ hosts: [
288
+ "soundcloud.com",
289
+ "m.soundcloud.com",
290
+ "on.soundcloud.com"
291
+ ],
292
+ types: [
293
+ "user",
294
+ "track",
295
+ "playlist"
296
+ ],
297
+ async parse(link, host, path) {
298
+ if (host === "on.soundcloud.com") {
299
+ if (!path[0] || path[1]) return null;
300
+ const { url, status } = await request({ url: link.slice(0, -1) });
301
+ return status === 200 ? await parseLink(url) : null;
302
+ } else {
303
+ const [user, second, track, fourth] = path;
304
+ let valid = false;
305
+ if (user && !second) valid = true;
306
+ else if (user && second && second !== "sets" && !track) valid = true;
307
+ else if (user && second === "sets" && track && !fourth) valid = true;
308
+ if (!valid) return null;
309
+ const data = (await request({
310
+ url: "https://soundcloud.com/oembed",
311
+ query: {
312
+ format: "json",
313
+ url: link.slice(0, -1)
314
+ }
315
+ })).json;
316
+ if (!data?.html) return null;
317
+ const rawUrl = data.html.match(/w\.soundcloud\.com.*?url=(.*?)[&"]/)?.[1];
318
+ if (!rawUrl) return null;
319
+ const splits = decodeURIComponent(rawUrl).split(/\/+/);
320
+ const kind = splits[2], id = splits[3];
321
+ if (!kind || !id) return null;
322
+ return {
323
+ service: this.name,
324
+ type: kind.slice(0, -1),
325
+ id
326
+ };
327
+ }
328
+ },
329
+ async render(type, id) {
330
+ const data = await parseWidget(type, id, false);
331
+ if (!data.id) return null;
332
+ const base = {
333
+ label: data.title ?? data.username,
334
+ sublabel: data.user?.username ?? "Top tracks",
335
+ explicit: Boolean(data.publisher_metadata?.explicit)
336
+ };
337
+ const thumbnailUrl = data.artwork_url ?? data.avatar_url;
338
+ if (type === "track") {
339
+ const audio = await parsePreview(data.media?.transcodings ?? []);
340
+ return {
341
+ ...base,
342
+ form: "single",
343
+ thumbnailUrl,
344
+ single: {
345
+ audio,
346
+ link: data.permalink_url
347
+ }
348
+ };
349
+ }
350
+ let tracks = [];
351
+ if (type === "user") {
352
+ const got = await parseWidget(type, id, true);
353
+ if (!got.collection) return null;
354
+ tracks = got.collection;
355
+ } else {
356
+ if (!data.tracks) return null;
357
+ tracks = data.tracks;
358
+ }
359
+ const list = [];
360
+ for (const track of tracks) {
361
+ if (!track.title || list.length >= PLAYLIST_LIMIT) continue;
362
+ const audio = await parsePreview(track.media?.transcodings ?? []);
363
+ list.push({
364
+ label: track.title,
365
+ sublabel: track.user?.username ?? "IDKK",
366
+ explicit: Boolean(track.publisher_metadata.explicit),
367
+ audio,
368
+ link: track.permalink_url
369
+ });
370
+ }
371
+ return {
372
+ ...base,
373
+ form: "list",
374
+ thumbnailUrl,
375
+ list
376
+ };
377
+ },
378
+ async from(type, id) {
379
+ const data = await parseWidget(type, id, false);
380
+ if (!data.id) return null;
381
+ return data.permalink_url;
382
+ }
383
+ };
384
+
385
+ //#endregion
386
+ //#region src/handlers/defs/services/spotify.ts
387
+ async function parseEmbed(type, id) {
388
+ return parseNextData((await request({ url: `https://open.spotify.com/embed/${type}/${id}` })).text);
389
+ }
390
+ function from(type, id) {
391
+ return `https://open.spotify.com/${type}/${id}`;
392
+ }
393
+ function fromUri(uri) {
394
+ const [sanityCheck, type, id] = uri.split(":");
395
+ if (sanityCheck === "spotify" && type && id) return from(type, id);
396
+ else return null;
397
+ }
398
+ const spotify = {
399
+ name: "spotify",
400
+ hosts: ["open.spotify.com"],
401
+ types: [
402
+ "track",
403
+ "album",
404
+ "playlist",
405
+ "artist"
406
+ ],
407
+ async parse(_link, _host, path) {
408
+ const [type, id, third] = path;
409
+ if (!type || !this.types.includes(type) || !id || third) return null;
410
+ const title = (await parseEmbed(type, id))?.props?.pageProps?.title;
411
+ if (title) return null;
412
+ return {
413
+ service: this.name,
414
+ type,
415
+ id
416
+ };
417
+ },
418
+ async render(type, id) {
419
+ const data = (await parseEmbed(type, id))?.props?.pageProps?.state?.data?.entity;
420
+ if (!data) return null;
421
+ const base = {
422
+ label: data.title,
423
+ sublabel: data.subtitle ?? data.artists?.map((x) => x.name).join(", "),
424
+ explicit: Boolean(data.isExplicit)
425
+ };
426
+ const thumbnailUrl = data.visualIdentity.image.sort((a, b) => a.maxWidth - b.maxWidth)[0]?.url;
427
+ if (type === "track") {
428
+ const link = fromUri(data.uri);
429
+ return {
430
+ ...base,
431
+ form: "single",
432
+ thumbnailUrl,
433
+ single: {
434
+ audio: data.audioPreview && data.duration ? {
435
+ duration: data.duration,
436
+ previewUrl: data.audioPreview.url
437
+ } : void 0,
438
+ link
439
+ }
440
+ };
441
+ } else {
442
+ const list = [];
443
+ for (const track of data.trackList ?? []) {
444
+ if (list.length >= PLAYLIST_LIMIT) continue;
445
+ const link = fromUri(track.uri);
446
+ list.push({
447
+ label: track.title,
448
+ sublabel: track.subtitle ?? track.artists?.map((x) => x.name).join(", "),
449
+ explicit: Boolean(track.isExplicit),
450
+ audio: track.audioPreview && track.duration ? {
451
+ duration: track.duration,
452
+ previewUrl: track.audioPreview.url
453
+ } : void 0,
454
+ link
455
+ });
456
+ }
457
+ return {
458
+ ...base,
459
+ form: "list",
460
+ thumbnailUrl,
461
+ list
462
+ };
463
+ }
464
+ },
465
+ from
466
+ };
467
+
468
+ //#endregion
469
+ //#region src/handlers/core.ts
470
+ const services = [
471
+ spotify,
472
+ soundcloud,
473
+ applemusic
474
+ ];
475
+ $.services = services;
476
+ const parsers = [songdotlink, ...services];
477
+ $.parsers = parsers;
478
+
479
+ //#endregion
480
+ export { $, parseLink, parsers, renderSong, services, toLink };
@@ -0,0 +1,55 @@
1
+ import { Song } from "./types-Cp4kDlRH.js";
2
+ import { MaybePromise } from "bun";
3
+
4
+ //#region src/handlers/types.d.ts
5
+ interface RenderInfoBase {
6
+ label: string;
7
+ sublabel: string;
8
+ explicit: boolean;
9
+ }
10
+ interface RenderInfoEntry {
11
+ audio?: {
12
+ duration: number;
13
+ previewUrl: string;
14
+ } | undefined;
15
+ link: string;
16
+ }
17
+ type RenderInfoEntryBased = RenderInfoEntry & RenderInfoBase;
18
+ interface RenderSongSingle extends RenderInfoBase {
19
+ form: "single";
20
+ thumbnailUrl?: string;
21
+ single: RenderInfoEntry;
22
+ }
23
+ interface RenderSongList extends RenderInfoBase {
24
+ form: "list";
25
+ thumbnailUrl?: string;
26
+ list: RenderInfoEntryBased[];
27
+ }
28
+ type RenderSongInfo = RenderSongSingle | RenderSongList;
29
+ //#endregion
30
+ //#region src/handlers/helpers.d.ts
31
+ interface SongParser {
32
+ hosts: string[];
33
+ parse(link: string, host: string, path: string[]): MaybePromise<Song | null>;
34
+ }
35
+ interface SongService extends SongParser {
36
+ name: string;
37
+ types: string[];
38
+ render(type: string, id: string): MaybePromise<RenderSongInfo | null>;
39
+ from(type: string, id: string): MaybePromise<string | null>;
40
+ }
41
+ //#endregion
42
+ //#region src/handlers/core.d.ts
43
+ declare const services: SongService[];
44
+ declare const parsers: SongParser[];
45
+ //#endregion
46
+ //#region src/handlers/finders.d.ts
47
+ declare const $: {
48
+ services: SongService[];
49
+ parsers: SongParser[];
50
+ };
51
+ declare function parseLink(link: string): Promise<Song | null>;
52
+ declare function renderSong(song: Song): Promise<RenderSongInfo | null>;
53
+ declare function toLink(song: Song): Promise<string | null>;
54
+ //#endregion
55
+ export { $, RenderInfoBase, RenderInfoEntry, RenderInfoEntryBased, RenderSongInfo, SongParser, SongService, parseLink, parsers, renderSong, services, toLink };
@@ -0,0 +1,3 @@
1
+ import { $, parseLink, parsers, renderSong, services, toLink } from "./core-CLy9m0H1.js";
2
+
3
+ export { $, parseLink, parsers, renderSong, services, toLink };
@@ -0,0 +1,13 @@
1
+ import { Song, UserData } from "./types-Cp4kDlRH.js";
2
+ import z, { ZodLiteral, ZodObject, ZodString, ZodUnion, core } from "zod";
3
+
4
+ //#region src/structs/zod.d.ts
5
+ type SongDef = ZodObject<{
6
+ service: ZodLiteral<string>;
7
+ type: ZodUnion<ZodLiteral<string>[]>;
8
+ id: ZodString;
9
+ }, core.$strip>;
10
+ declare const SongSchema: z.ZodDiscriminatedUnion<[SongDef], "service">;
11
+ declare const UserDataSchema: z.ZodArray<z.ZodDiscriminatedUnion<[SongDef], "service">>;
12
+ //#endregion
13
+ export { Song, SongSchema, UserData, UserDataSchema };
@@ -0,0 +1,13 @@
1
+ import { services } from "./core-CLy9m0H1.js";
2
+ import z from "zod";
3
+
4
+ //#region src/structs/zod.ts
5
+ const SongSchema = z.discriminatedUnion("service", services.map((service) => z.object({
6
+ service: z.literal(service.name),
7
+ type: z.union(service.types.map((type) => z.literal(type))),
8
+ id: z.string()
9
+ })));
10
+ const UserDataSchema = z.array(SongSchema).max(6);
11
+
12
+ //#endregion
13
+ export { SongSchema, UserDataSchema };
@@ -0,0 +1,9 @@
1
+ //#region src/structs/types.d.ts
2
+ interface Song {
3
+ service: string;
4
+ type: string;
5
+ id: string;
6
+ }
7
+ type UserData = Song[];
8
+ //#endregion
9
+ export { Song, UserData };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@song-spotlight/api",
3
+ "version": "0.1.0",
4
+ "description": "Song Spotlight API types and song validation module",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "rm -rf dist && rolldown -c rolldown.config.mts",
8
+ "prepublishOnly": "bun run build"
9
+ },
10
+ "exports": {
11
+ "./handlers": {
12
+ "default": "./dist/handlers.js",
13
+ "types": "./dist/handlers.d.ts"
14
+ },
15
+ "./structs": {
16
+ "default": "./dist/structs.js",
17
+ "types": "./dist/structs.d.ts"
18
+ }
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "license": "MIT",
27
+ "homepage": "https://github.com/nexpid-labs/SongSpotlight/tree/main/packages/api",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/nexpid-labs/SongSpotlight.git",
31
+ "directory": "packages/api"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/nexpid-labs/SongSpotlight/issues"
35
+ },
36
+ "dependencies": {
37
+ "zod": "^4.1.5"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "latest",
41
+ "rolldown": "^1.0.0-beta.34",
42
+ "rolldown-plugin-dts": "^0.15.10"
43
+ },
44
+ "peerDependencies": {
45
+ "typescript": "^5"
46
+ }
47
+ }