@saavn-labs/sdk 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 +21 -0
- package/README.md +339 -0
- package/dist/core/models/album.d.ts +32 -0
- package/dist/core/models/album.js +1 -0
- package/dist/core/models/artist.d.ts +61 -0
- package/dist/core/models/artist.js +1 -0
- package/dist/core/models/index.d.ts +4 -0
- package/dist/core/models/index.js +4 -0
- package/dist/core/models/playlist.d.ts +35 -0
- package/dist/core/models/playlist.js +1 -0
- package/dist/core/models/song.d.ts +52 -0
- package/dist/core/models/song.js +1 -0
- package/dist/core/modules/album.module.d.ts +82 -0
- package/dist/core/modules/album.module.js +74 -0
- package/dist/core/modules/artist.module.d.ts +97 -0
- package/dist/core/modules/artist.module.js +49 -0
- package/dist/core/modules/extras.module.d.ts +385 -0
- package/dist/core/modules/extras.module.js +64 -0
- package/dist/core/modules/playlist.module.d.ts +79 -0
- package/dist/core/modules/playlist.module.js +74 -0
- package/dist/core/modules/song.module.d.ts +91 -0
- package/dist/core/modules/song.module.js +97 -0
- package/dist/helpers/errors.d.ts +25 -0
- package/dist/helpers/errors.js +84 -0
- package/dist/helpers/experimental/stream-urls/index.d.ts +20 -0
- package/dist/helpers/experimental/stream-urls/index.js +33 -0
- package/dist/helpers/experimental/stream-urls/stream-urls.edge.d.ts +4 -0
- package/dist/helpers/experimental/stream-urls/stream-urls.edge.js +38 -0
- package/dist/helpers/experimental/stream-urls/stream-urls.node.d.ts +4 -0
- package/dist/helpers/experimental/stream-urls/stream-urls.node.js +27 -0
- package/dist/helpers/fetch.d.ts +38 -0
- package/dist/helpers/fetch.js +38 -0
- package/dist/helpers/utils.d.ts +13 -0
- package/dist/helpers/utils.js +58 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/saavn/common-mapper.d.ts +77 -0
- package/dist/saavn/common-mapper.js +348 -0
- package/dist/saavn/entities/album.entity.d.ts +151 -0
- package/dist/saavn/entities/album.entity.js +25 -0
- package/dist/saavn/entities/artist.entity.d.ts +274 -0
- package/dist/saavn/entities/artist.entity.js +49 -0
- package/dist/saavn/entities/base.d.ts +117 -0
- package/dist/saavn/entities/base.js +71 -0
- package/dist/saavn/entities/extras.d.ts +102 -0
- package/dist/saavn/entities/extras.js +43 -0
- package/dist/saavn/entities/index.d.ts +6 -0
- package/dist/saavn/entities/index.js +6 -0
- package/dist/saavn/entities/playlist.entity.d.ts +143 -0
- package/dist/saavn/entities/playlist.entity.js +55 -0
- package/dist/saavn/entities/show.entity.d.ts +14 -0
- package/dist/saavn/entities/show.entity.js +14 -0
- package/dist/saavn/entities/song.entity.d.ts +142 -0
- package/dist/saavn/entities/song.entity.js +48 -0
- package/dist/saavn/operations/get-details/index.d.ts +755 -0
- package/dist/saavn/operations/get-details/index.js +34 -0
- package/dist/saavn/operations/get-details/mapper.ops.d.ts +35 -0
- package/dist/saavn/operations/get-details/mapper.ops.js +55 -0
- package/dist/saavn/operations/get-details/schema.ops.d.ts +706 -0
- package/dist/saavn/operations/get-details/schema.ops.js +49 -0
- package/dist/saavn/operations/get-reco/index.d.ts +144 -0
- package/dist/saavn/operations/get-reco/index.js +19 -0
- package/dist/saavn/operations/get-reco/mapper.ops.d.ts +36 -0
- package/dist/saavn/operations/get-reco/mapper.ops.js +43 -0
- package/dist/saavn/operations/get-reco/schema.ops.d.ts +103 -0
- package/dist/saavn/operations/get-reco/schema.ops.js +28 -0
- package/dist/saavn/operations/get-trending/index.d.ts +288 -0
- package/dist/saavn/operations/get-trending/index.js +24 -0
- package/dist/saavn/operations/get-trending/mapper.ops.d.ts +13 -0
- package/dist/saavn/operations/get-trending/mapper.ops.js +134 -0
- package/dist/saavn/operations/get-trending/schema.ops.d.ts +272 -0
- package/dist/saavn/operations/get-trending/schema.ops.js +34 -0
- package/dist/saavn/operations/index.d.ts +6 -0
- package/dist/saavn/operations/index.js +6 -0
- package/dist/saavn/operations/search-results/index.d.ts +790 -0
- package/dist/saavn/operations/search-results/index.js +29 -0
- package/dist/saavn/operations/search-results/mapper.ops.d.ts +413 -0
- package/dist/saavn/operations/search-results/mapper.ops.js +80 -0
- package/dist/saavn/operations/search-results/schema.ops.d.ts +366 -0
- package/dist/saavn/operations/search-results/schema.ops.js +101 -0
- package/dist/saavn/operations/web-api/index.d.ts +604 -0
- package/dist/saavn/operations/web-api/index.js +29 -0
- package/dist/saavn/operations/web-api/mapper.ops.d.ts +11 -0
- package/dist/saavn/operations/web-api/mapper.ops.js +23 -0
- package/dist/saavn/operations/web-api/schema.ops.d.ts +582 -0
- package/dist/saavn/operations/web-api/schema.ops.js +50 -0
- package/dist/saavn/operations/web-radio/index.d.ts +181 -0
- package/dist/saavn/operations/web-radio/index.js +19 -0
- package/dist/saavn/operations/web-radio/mapper.ops.d.ts +14 -0
- package/dist/saavn/operations/web-radio/mapper.ops.js +35 -0
- package/dist/saavn/operations/web-radio/schema.ops.d.ts +162 -0
- package/dist/saavn/operations/web-radio/schema.ops.js +56 -0
- package/dist/saavn/primitives/enums.d.ts +17 -0
- package/dist/saavn/primitives/enums.js +4 -0
- package/dist/saavn/primitives/string.d.ts +5 -0
- package/dist/saavn/primitives/string.js +22 -0
- package/dist/saavn/run-operation.d.ts +12 -0
- package/dist/saavn/run-operation.js +39 -0
- package/dist/schemas/index.d.ts +2188 -0
- package/dist/schemas/index.js +14 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch a playlist by Saavn ID.
|
|
3
|
+
*
|
|
4
|
+
* @param params - Parameters object
|
|
5
|
+
* @param params.playlistId - Saavn playlist ID
|
|
6
|
+
* @returns Normalized playlist object
|
|
7
|
+
*/
|
|
8
|
+
declare function getById({ playlistId }: {
|
|
9
|
+
playlistId: string;
|
|
10
|
+
}): Promise<import("../models").Playlist>;
|
|
11
|
+
/**
|
|
12
|
+
* Fetch a playlist by permalink.
|
|
13
|
+
*
|
|
14
|
+
* @param params - Parameters object
|
|
15
|
+
* @param params.permalink - Playlist permalink string
|
|
16
|
+
* @returns Normalized playlist object
|
|
17
|
+
*/
|
|
18
|
+
declare function getByPermalink({ permalink }: {
|
|
19
|
+
permalink: string;
|
|
20
|
+
}): Promise<import("../models").Playlist>;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch playlist recommendations by Saavn ID.
|
|
23
|
+
*
|
|
24
|
+
* @param params - Parameters object
|
|
25
|
+
* @param params.playlistId - Saavn playlist ID
|
|
26
|
+
* @returns List of recommended playlists
|
|
27
|
+
*/
|
|
28
|
+
declare function getRecommendations({ playlistId }: {
|
|
29
|
+
playlistId: string;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
id: string;
|
|
32
|
+
type: "playlist";
|
|
33
|
+
title: string;
|
|
34
|
+
subtitle: string;
|
|
35
|
+
url: string;
|
|
36
|
+
images: import("../..").Image[];
|
|
37
|
+
flags: {
|
|
38
|
+
isExplicit: boolean;
|
|
39
|
+
};
|
|
40
|
+
owner: {
|
|
41
|
+
name: string;
|
|
42
|
+
};
|
|
43
|
+
}[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Get trending playlists by language.
|
|
46
|
+
*
|
|
47
|
+
* @param params - Parameters object
|
|
48
|
+
* @param params.language - Language code
|
|
49
|
+
* @returns List of trending playlists
|
|
50
|
+
*/
|
|
51
|
+
declare function getTrending({ language }: {
|
|
52
|
+
language: string;
|
|
53
|
+
}): Promise<import("../models").Playlist[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Search for playlists by query.
|
|
56
|
+
*
|
|
57
|
+
* @param params - Parameters object
|
|
58
|
+
* @param params.query - Search query string
|
|
59
|
+
* @param params.limit - Number of results to return (default: 10)
|
|
60
|
+
* @param params.offset - Offset for pagination (default: 1)
|
|
61
|
+
* @returns List of matching playlists
|
|
62
|
+
*/
|
|
63
|
+
declare function search({ query, limit, offset, }: {
|
|
64
|
+
query: string;
|
|
65
|
+
limit?: number;
|
|
66
|
+
offset?: number;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
total: number;
|
|
69
|
+
start: number;
|
|
70
|
+
results: import("../models").Playlist[];
|
|
71
|
+
}>;
|
|
72
|
+
export declare const PlaylistModule: {
|
|
73
|
+
getById: typeof getById;
|
|
74
|
+
getByPermalink: typeof getByPermalink;
|
|
75
|
+
getRecommendations: typeof getRecommendations;
|
|
76
|
+
getTrending: typeof getTrending;
|
|
77
|
+
search: typeof search;
|
|
78
|
+
};
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { SaavnGetDetails, SaavnGetReco, SaavnGetTrending, SaavnSearchResults, SaavnWebAPI, } from '../../saavn/operations';
|
|
2
|
+
import { runOperation } from '../../saavn/run-operation';
|
|
3
|
+
import { extractPermalinkToken } from '../../helpers/utils';
|
|
4
|
+
import { SDKError } from '../../helpers/errors';
|
|
5
|
+
/**
|
|
6
|
+
* Fetch a playlist by Saavn ID.
|
|
7
|
+
*
|
|
8
|
+
* @param params - Parameters object
|
|
9
|
+
* @param params.playlistId - Saavn playlist ID
|
|
10
|
+
* @returns Normalized playlist object
|
|
11
|
+
*/
|
|
12
|
+
function getById({ playlistId }) {
|
|
13
|
+
return runOperation(SaavnGetDetails.playlist, { listid: playlistId });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Fetch a playlist by permalink.
|
|
17
|
+
*
|
|
18
|
+
* @param params - Parameters object
|
|
19
|
+
* @param params.permalink - Playlist permalink string
|
|
20
|
+
* @returns Normalized playlist object
|
|
21
|
+
*/
|
|
22
|
+
function getByPermalink({ permalink }) {
|
|
23
|
+
const { token, type } = extractPermalinkToken(permalink) ?? {};
|
|
24
|
+
if (!token || type !== 'playlist') {
|
|
25
|
+
throw new SDKError('INVALID_PARAMS', 'Invalid permalink provided');
|
|
26
|
+
}
|
|
27
|
+
return runOperation(SaavnWebAPI.playlist, { token, type: 'playlist' });
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fetch playlist recommendations by Saavn ID.
|
|
31
|
+
*
|
|
32
|
+
* @param params - Parameters object
|
|
33
|
+
* @param params.playlistId - Saavn playlist ID
|
|
34
|
+
* @returns List of recommended playlists
|
|
35
|
+
*/
|
|
36
|
+
function getRecommendations({ playlistId }) {
|
|
37
|
+
return runOperation(SaavnGetReco.playlists, { listid: playlistId });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get trending playlists by language.
|
|
41
|
+
*
|
|
42
|
+
* @param params - Parameters object
|
|
43
|
+
* @param params.language - Language code
|
|
44
|
+
* @returns List of trending playlists
|
|
45
|
+
*/
|
|
46
|
+
function getTrending({ language }) {
|
|
47
|
+
return runOperation(SaavnGetTrending.playlists, {
|
|
48
|
+
entity_language: language,
|
|
49
|
+
entity_type: 'playlist',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Search for playlists by query.
|
|
54
|
+
*
|
|
55
|
+
* @param params - Parameters object
|
|
56
|
+
* @param params.query - Search query string
|
|
57
|
+
* @param params.limit - Number of results to return (default: 10)
|
|
58
|
+
* @param params.offset - Offset for pagination (default: 1)
|
|
59
|
+
* @returns List of matching playlists
|
|
60
|
+
*/
|
|
61
|
+
function search({ query, limit = 20, offset = 1, }) {
|
|
62
|
+
return runOperation(SaavnSearchResults.playlists, {
|
|
63
|
+
q: query,
|
|
64
|
+
p: String(offset),
|
|
65
|
+
n: String(limit),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export const PlaylistModule = {
|
|
69
|
+
getById,
|
|
70
|
+
getByPermalink,
|
|
71
|
+
getRecommendations,
|
|
72
|
+
getTrending,
|
|
73
|
+
search,
|
|
74
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { fetchStreamUrls } from '../../helpers/experimental/stream-urls';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch a song or songs by Saavn ID.
|
|
4
|
+
*
|
|
5
|
+
* @param params - Parameters object
|
|
6
|
+
* @param params.songIds - Saavn song ID or array of IDs
|
|
7
|
+
* @returns Normalized song object or list
|
|
8
|
+
*/
|
|
9
|
+
declare function getById({ songIds }: {
|
|
10
|
+
songIds: string[] | string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
songs: import("../models").Song[];
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Fetch a song by permalink.
|
|
16
|
+
*
|
|
17
|
+
* @param params - Parameters object
|
|
18
|
+
* @param params.permalink - Song permalink string
|
|
19
|
+
* @returns Normalized song object
|
|
20
|
+
*/
|
|
21
|
+
declare function getByPermalink({ permalink }: {
|
|
22
|
+
permalink: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
songs: import("../models").Song[];
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Fetch songs by station ID.
|
|
28
|
+
*
|
|
29
|
+
* @param params - Parameters object
|
|
30
|
+
* @param params.stationId - Station ID
|
|
31
|
+
* @param params.limit - Number of songs to fetch (default: 10)
|
|
32
|
+
* @param params.next - Whether to get next songs (optional)
|
|
33
|
+
* @returns List of songs from station
|
|
34
|
+
*/
|
|
35
|
+
declare function getByStationId({ stationId, limit, next, }: {
|
|
36
|
+
stationId: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
next?: boolean;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
stationId: string | undefined;
|
|
41
|
+
songs: import("../models").Song[];
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Fetch song recommendations by Saavn ID.
|
|
45
|
+
*
|
|
46
|
+
* @param params - Parameters object
|
|
47
|
+
* @param params.songId - Saavn song ID
|
|
48
|
+
* @returns List of recommended songs
|
|
49
|
+
*/
|
|
50
|
+
declare function getRecommendations({ songId }: {
|
|
51
|
+
songId: string;
|
|
52
|
+
}): Promise<import("../models").Song[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Get trending songs by language.
|
|
55
|
+
* @param params - Parameters object
|
|
56
|
+
* @param params.language - Language code
|
|
57
|
+
* @returns List of trending songs
|
|
58
|
+
*/
|
|
59
|
+
declare function getTrending({ language }: {
|
|
60
|
+
language: string;
|
|
61
|
+
}): Promise<import("../models").Song[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Search for songs by query.
|
|
64
|
+
*
|
|
65
|
+
* @param params - Parameters object
|
|
66
|
+
* @param params.query - Search query string
|
|
67
|
+
* @param params.limit - Number of results to return (default: 10)
|
|
68
|
+
* @param params.offset - Offset for pagination (default: 1)
|
|
69
|
+
* @returns List of matching songs
|
|
70
|
+
*/
|
|
71
|
+
declare function search({ query, limit, offset, }: {
|
|
72
|
+
query: string;
|
|
73
|
+
limit?: number;
|
|
74
|
+
offset?: number;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
total: number;
|
|
77
|
+
start: number;
|
|
78
|
+
results: import("../models").Song[];
|
|
79
|
+
}>;
|
|
80
|
+
export declare const SongModule: {
|
|
81
|
+
getById: typeof getById;
|
|
82
|
+
getByPermalink: typeof getByPermalink;
|
|
83
|
+
getByStationId: typeof getByStationId;
|
|
84
|
+
getRecommendations: typeof getRecommendations;
|
|
85
|
+
getTrending: typeof getTrending;
|
|
86
|
+
search: typeof search;
|
|
87
|
+
experimental: {
|
|
88
|
+
fetchStreamUrls: typeof fetchStreamUrls;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { SaavnGetDetails, SaavnGetReco, SaavnGetTrending, SaavnSearchResults, SaavnWebAPI, SaavnWebRadio, } from '../../saavn/operations';
|
|
2
|
+
import { runOperation } from '../../saavn/run-operation';
|
|
3
|
+
import { extractPermalinkToken } from '../../helpers/utils';
|
|
4
|
+
import { fetchStreamUrls } from '../../helpers/experimental/stream-urls';
|
|
5
|
+
import { SDKError } from '../../helpers/errors';
|
|
6
|
+
/**
|
|
7
|
+
* Fetch a song or songs by Saavn ID.
|
|
8
|
+
*
|
|
9
|
+
* @param params - Parameters object
|
|
10
|
+
* @param params.songIds - Saavn song ID or array of IDs
|
|
11
|
+
* @returns Normalized song object or list
|
|
12
|
+
*/
|
|
13
|
+
function getById({ songIds }) {
|
|
14
|
+
return runOperation(SaavnGetDetails.songs, {
|
|
15
|
+
pids: songIds instanceof Array ? songIds.join(',') : songIds,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fetch a song by permalink.
|
|
20
|
+
*
|
|
21
|
+
* @param params - Parameters object
|
|
22
|
+
* @param params.permalink - Song permalink string
|
|
23
|
+
* @returns Normalized song object
|
|
24
|
+
*/
|
|
25
|
+
function getByPermalink({ permalink }) {
|
|
26
|
+
const { token, type } = extractPermalinkToken(permalink) ?? {};
|
|
27
|
+
if (!token || type !== 'song') {
|
|
28
|
+
throw new SDKError('INVALID_PARAMS', 'Invalid permalink provided');
|
|
29
|
+
}
|
|
30
|
+
return runOperation(SaavnWebAPI.songs, { token, type: 'song' });
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fetch songs by station ID.
|
|
34
|
+
*
|
|
35
|
+
* @param params - Parameters object
|
|
36
|
+
* @param params.stationId - Station ID
|
|
37
|
+
* @param params.limit - Number of songs to fetch (default: 10)
|
|
38
|
+
* @param params.next - Whether to get next songs (optional)
|
|
39
|
+
* @returns List of songs from station
|
|
40
|
+
*/
|
|
41
|
+
function getByStationId({ stationId, limit = 20, next, }) {
|
|
42
|
+
return runOperation(SaavnWebRadio.songs, {
|
|
43
|
+
ctx: 'android',
|
|
44
|
+
stationid: stationId,
|
|
45
|
+
k: String(limit),
|
|
46
|
+
next: next ? '1' : '0',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fetch song recommendations by Saavn ID.
|
|
51
|
+
*
|
|
52
|
+
* @param params - Parameters object
|
|
53
|
+
* @param params.songId - Saavn song ID
|
|
54
|
+
* @returns List of recommended songs
|
|
55
|
+
*/
|
|
56
|
+
function getRecommendations({ songId }) {
|
|
57
|
+
return runOperation(SaavnGetReco.songs, { pid: songId });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get trending songs by language.
|
|
61
|
+
* @param params - Parameters object
|
|
62
|
+
* @param params.language - Language code
|
|
63
|
+
* @returns List of trending songs
|
|
64
|
+
*/
|
|
65
|
+
function getTrending({ language }) {
|
|
66
|
+
return runOperation(SaavnGetTrending.songs, {
|
|
67
|
+
entity_language: language,
|
|
68
|
+
entity_type: 'song',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Search for songs by query.
|
|
73
|
+
*
|
|
74
|
+
* @param params - Parameters object
|
|
75
|
+
* @param params.query - Search query string
|
|
76
|
+
* @param params.limit - Number of results to return (default: 10)
|
|
77
|
+
* @param params.offset - Offset for pagination (default: 1)
|
|
78
|
+
* @returns List of matching songs
|
|
79
|
+
*/
|
|
80
|
+
function search({ query, limit = 20, offset = 1, }) {
|
|
81
|
+
return runOperation(SaavnSearchResults.songs, {
|
|
82
|
+
q: query,
|
|
83
|
+
p: String(offset),
|
|
84
|
+
n: String(limit),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export const SongModule = {
|
|
88
|
+
getById,
|
|
89
|
+
getByPermalink,
|
|
90
|
+
getByStationId,
|
|
91
|
+
getRecommendations,
|
|
92
|
+
getTrending,
|
|
93
|
+
search,
|
|
94
|
+
experimental: {
|
|
95
|
+
fetchStreamUrls,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error utilities for @saavn-labs/sdk
|
|
3
|
+
* @module errors
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import { ZodError } from 'zod';
|
|
7
|
+
export type SDKErrorCode = 'INVALID_PARAMS' | 'INVALID_RESPONSE' | 'API_ERROR' | 'NETWORK_ERROR' | 'UNSUPPORTED_RUNTIME' | 'EXPERIMENTAL_FEATURE' | 'INTERNAL_ERROR';
|
|
8
|
+
export declare class SDKError extends Error {
|
|
9
|
+
readonly code: SDKErrorCode;
|
|
10
|
+
readonly details?: unknown;
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
constructor(code: SDKErrorCode, message: string, options?: {
|
|
13
|
+
details?: unknown;
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export declare function fromZodError(error: ZodError, kind: 'params' | 'response', context?: string): SDKError;
|
|
18
|
+
export declare function networkError(context?: string, status?: number): SDKError;
|
|
19
|
+
export declare function experimentalError(message?: string): SDKError;
|
|
20
|
+
export declare function internalError(message?: string, cause?: unknown): SDKError;
|
|
21
|
+
/**
|
|
22
|
+
* Detect unusable API responses before schema validation.
|
|
23
|
+
* Returns a user-friendly message or null if response looks valid.
|
|
24
|
+
*/
|
|
25
|
+
export declare function detectSaavnApiError(data: unknown): string | null;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error utilities for @saavn-labs/sdk
|
|
3
|
+
* @module errors
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import { ZodError } from 'zod';
|
|
7
|
+
export class SDKError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
details;
|
|
10
|
+
cause;
|
|
11
|
+
constructor(code, message, options) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'SDKError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.details = options?.details;
|
|
16
|
+
this.cause = options?.cause;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function fromZodError(error, kind, context) {
|
|
20
|
+
return new SDKError(kind === 'params' ? 'INVALID_PARAMS' : 'INVALID_RESPONSE', `Invalid ${kind}${context ? ` for ${context}` : ''}.`, {
|
|
21
|
+
details: error.issues.map((issue) => ({
|
|
22
|
+
path: issue.path.join('.'),
|
|
23
|
+
message: issue.message,
|
|
24
|
+
})),
|
|
25
|
+
cause: error,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function networkError(context, status) {
|
|
29
|
+
return new SDKError('NETWORK_ERROR', `Request failed${context ? ` for ${context}` : ''}.`, {
|
|
30
|
+
details: status ? { status } : undefined,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function experimentalError(message) {
|
|
34
|
+
return new SDKError('EXPERIMENTAL_FEATURE', message ??
|
|
35
|
+
'This feature is experimental and requires explicit user acknowledgment.');
|
|
36
|
+
}
|
|
37
|
+
export function internalError(message = 'An internal error occurred.', cause) {
|
|
38
|
+
return new SDKError('INTERNAL_ERROR', message, { cause });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Detect unusable API responses before schema validation.
|
|
42
|
+
* Returns a user-friendly message or null if response looks valid.
|
|
43
|
+
*/
|
|
44
|
+
export function detectSaavnApiError(data) {
|
|
45
|
+
if (data == null) {
|
|
46
|
+
return 'Empty response from Saavn API.';
|
|
47
|
+
}
|
|
48
|
+
if (data === '' || data === false) {
|
|
49
|
+
return 'Invalid response from Saavn API.';
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
52
|
+
return 'No data returned from Saavn API.';
|
|
53
|
+
}
|
|
54
|
+
if (typeof data !== 'object') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const obj = data;
|
|
58
|
+
if (typeof obj.error === 'string') {
|
|
59
|
+
return obj.error;
|
|
60
|
+
}
|
|
61
|
+
if (typeof obj.error === 'object' && obj.error !== null) {
|
|
62
|
+
const err = obj.error;
|
|
63
|
+
if (typeof err.msg === 'string') {
|
|
64
|
+
return err.msg;
|
|
65
|
+
}
|
|
66
|
+
if (typeof err.code === 'string') {
|
|
67
|
+
switch (err.code) {
|
|
68
|
+
case 'INPUT_MISSING':
|
|
69
|
+
return 'One or more required parameters are missing.';
|
|
70
|
+
default:
|
|
71
|
+
return `Saavn API error (${err.code}).`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return 'Saavn API returned an error.';
|
|
75
|
+
}
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(obj, 'stationid') &&
|
|
77
|
+
obj.stationid === '') {
|
|
78
|
+
return 'Invalid or expired station ID.';
|
|
79
|
+
}
|
|
80
|
+
if (typeof obj.status === 'string' && obj.status !== 'success') {
|
|
81
|
+
return `Saavn API returned status: ${obj.status}.`;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Runtime = 'node' | 'edge';
|
|
2
|
+
/**
|
|
3
|
+
* ⚠️ EXPERIMENTAL
|
|
4
|
+
*
|
|
5
|
+
* Generates direct download links for a song.
|
|
6
|
+
*
|
|
7
|
+
* This API behaves differently depending on runtime:
|
|
8
|
+
* - `node`: uses local DES-ECB decryption via Node crypto
|
|
9
|
+
* - `edge`: uses Saavn's undocumented internal API
|
|
10
|
+
*
|
|
11
|
+
* Runtime must be specified explicitly.
|
|
12
|
+
*
|
|
13
|
+
* @param encryptedUrl - Encrypted media URL
|
|
14
|
+
* @param options - { runtime: 'node' | 'edge', acknowledge: boolean } to acknowledge experimental nature
|
|
15
|
+
* @returns Stream URL
|
|
16
|
+
*/
|
|
17
|
+
export declare function fetchStreamUrls(encryptedUrl: string, runtime: Runtime, acknowledge: boolean): Promise<Array<{
|
|
18
|
+
bitrate: string;
|
|
19
|
+
url: string;
|
|
20
|
+
}>>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SDKError, experimentalError } from '../../../helpers/errors';
|
|
2
|
+
import { generateStreamUrlsNode } from './stream-urls.node';
|
|
3
|
+
import { generateStreamUrlsEdge } from './stream-urls.edge';
|
|
4
|
+
/**
|
|
5
|
+
* ⚠️ EXPERIMENTAL
|
|
6
|
+
*
|
|
7
|
+
* Generates direct download links for a song.
|
|
8
|
+
*
|
|
9
|
+
* This API behaves differently depending on runtime:
|
|
10
|
+
* - `node`: uses local DES-ECB decryption via Node crypto
|
|
11
|
+
* - `edge`: uses Saavn's undocumented internal API
|
|
12
|
+
*
|
|
13
|
+
* Runtime must be specified explicitly.
|
|
14
|
+
*
|
|
15
|
+
* @param encryptedUrl - Encrypted media URL
|
|
16
|
+
* @param options - { runtime: 'node' | 'edge', acknowledge: boolean } to acknowledge experimental nature
|
|
17
|
+
* @returns Stream URL
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchStreamUrls(encryptedUrl, runtime, acknowledge) {
|
|
20
|
+
if (!encryptedUrl)
|
|
21
|
+
return [];
|
|
22
|
+
if (acknowledge !== true) {
|
|
23
|
+
throw experimentalError('Stream URL generation is experimental and requires explicit acknowledgment.');
|
|
24
|
+
}
|
|
25
|
+
switch (runtime) {
|
|
26
|
+
case 'node':
|
|
27
|
+
return generateStreamUrlsNode(encryptedUrl);
|
|
28
|
+
case 'edge':
|
|
29
|
+
return generateStreamUrlsEdge(encryptedUrl);
|
|
30
|
+
default:
|
|
31
|
+
throw new SDKError('UNSUPPORTED_RUNTIME', `Unsupported runtime: ${String(runtime)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fetchFromSaavn } from '../../../helpers/fetch';
|
|
2
|
+
const AUDIO_QUALITIES = [
|
|
3
|
+
{ id: '_12', bitrate: '12kbps' },
|
|
4
|
+
{ id: '_48', bitrate: '48kbps' },
|
|
5
|
+
{ id: '_96', bitrate: '96kbps' },
|
|
6
|
+
{ id: '_160', bitrate: '160kbps' },
|
|
7
|
+
{ id: '_320', bitrate: '320kbps' },
|
|
8
|
+
];
|
|
9
|
+
function normalizeAacUrl(rawUrl) {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(rawUrl);
|
|
12
|
+
url.hostname = url.hostname.replace(/^web\./, 'aac.');
|
|
13
|
+
return url.toString();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return rawUrl;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function generateStreamUrlsEdge(encryptedUrl) {
|
|
20
|
+
const res = await fetchFromSaavn({
|
|
21
|
+
call: 'song.generateAuthToken',
|
|
22
|
+
params: {
|
|
23
|
+
url: encryptedUrl,
|
|
24
|
+
bitrate: '96',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok)
|
|
28
|
+
return [];
|
|
29
|
+
const data = res.data;
|
|
30
|
+
if (typeof data.auth_url === 'string') {
|
|
31
|
+
const baseUrl = normalizeAacUrl(data.auth_url.split('?')[0]);
|
|
32
|
+
return AUDIO_QUALITIES.map((q) => ({
|
|
33
|
+
bitrate: q.bitrate,
|
|
34
|
+
url: baseUrl.replace('_96', q.id),
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const AUDIO_QUALITIES = [
|
|
3
|
+
{ id: '_12', bitrate: '12kbps' },
|
|
4
|
+
{ id: '_48', bitrate: '48kbps' },
|
|
5
|
+
{ id: '_96', bitrate: '96kbps' },
|
|
6
|
+
{ id: '_160', bitrate: '160kbps' },
|
|
7
|
+
{ id: '_320', bitrate: '320kbps' },
|
|
8
|
+
];
|
|
9
|
+
export function generateStreamUrlsNode(encryptedUrl) {
|
|
10
|
+
if (!encryptedUrl)
|
|
11
|
+
return [];
|
|
12
|
+
try {
|
|
13
|
+
const key = Buffer.from('38346591', 'utf8');
|
|
14
|
+
const decipher = crypto.createDecipheriv('des-ecb', key, null);
|
|
15
|
+
decipher.setAutoPadding(true);
|
|
16
|
+
const decrypted = decipher.update(encryptedUrl, 'base64', 'utf8') + decipher.final('utf8');
|
|
17
|
+
if (!decrypted)
|
|
18
|
+
return [];
|
|
19
|
+
return AUDIO_QUALITIES.map((q) => ({
|
|
20
|
+
bitrate: q.bitrate,
|
|
21
|
+
url: decrypted.replace('_96', q.id),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch utility for @saavn-labs/sdk
|
|
3
|
+
* @module fetch
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* API context/environment to use for requests
|
|
8
|
+
* - 'web6dot0': Web context
|
|
9
|
+
* - 'android': Android context
|
|
10
|
+
*/
|
|
11
|
+
export type ClientContext = 'web6dot0' | 'android';
|
|
12
|
+
/**
|
|
13
|
+
* Parameters for making an API request to JioSaavn
|
|
14
|
+
*/
|
|
15
|
+
export interface FetchParams {
|
|
16
|
+
/** API call to op (e.g., 'song.getDetails') */
|
|
17
|
+
call: string;
|
|
18
|
+
/** Query parameters for the API call */
|
|
19
|
+
params?: Record<string, string | number | boolean>;
|
|
20
|
+
/** API context to use (default: 'web6dot0') */
|
|
21
|
+
context?: ClientContext;
|
|
22
|
+
/** Base URL for the JioSaavn API (e.g. your own proxy) */
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
/** Custom HTTP headers to include in request */
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
/** Custom User-Agent strings to rotate from (optional) */
|
|
27
|
+
userAgents?: string[];
|
|
28
|
+
/** Custom fetch implementation (e.g., for environments without global fetch) */
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
/** Optional timeout in milliseconds */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface FetchResponse {
|
|
34
|
+
data: unknown;
|
|
35
|
+
ok: boolean;
|
|
36
|
+
status: number;
|
|
37
|
+
}
|
|
38
|
+
export declare const fetchFromSaavn: ({ call, params, context, baseUrl, headers, userAgents, timeoutMs, fetch: fetchImpl, }: FetchParams) => Promise<FetchResponse>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch utility for @saavn-labs/sdk
|
|
3
|
+
* @module fetch
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import { pickUserAgent } from './utils';
|
|
7
|
+
const BASE_URL = 'https://www.jiosaavn.com';
|
|
8
|
+
export const fetchFromSaavn = async ({ call, params = {}, context = 'web6dot0', baseUrl = BASE_URL, headers = {}, userAgents, timeoutMs, fetch: fetchImpl = globalThis.fetch, }) => {
|
|
9
|
+
const url = new URL(`${baseUrl}/api.php`);
|
|
10
|
+
url.searchParams.append('__call', call);
|
|
11
|
+
url.searchParams.append('_format', 'json');
|
|
12
|
+
url.searchParams.append('_marker', '0');
|
|
13
|
+
url.searchParams.append('api_version', '4');
|
|
14
|
+
url.searchParams.append('ctx', context);
|
|
15
|
+
for (const [k, v] of Object.entries(params)) {
|
|
16
|
+
url.searchParams.append(k, String(v));
|
|
17
|
+
}
|
|
18
|
+
const controller = timeoutMs ? new AbortController() : undefined;
|
|
19
|
+
const timer = timeoutMs
|
|
20
|
+
? setTimeout(() => controller?.abort(), timeoutMs)
|
|
21
|
+
: undefined;
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetchImpl(url.toString(), {
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
'User-Agent': pickUserAgent(userAgents),
|
|
27
|
+
...headers,
|
|
28
|
+
},
|
|
29
|
+
signal: controller?.signal,
|
|
30
|
+
});
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
return { data, ok: res.ok, status: res.status };
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (timer)
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General utilities for @saavn-labs/sdk
|
|
3
|
+
* @module utils
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import type { SaavnPermalinkToken } from '../types';
|
|
7
|
+
export declare function pickUserAgent(custom?: readonly string[]): string;
|
|
8
|
+
/**
|
|
9
|
+
* Extract entity type and token from a JioSaavn share URL
|
|
10
|
+
* @param permaLink - JioSaavn share URL or token
|
|
11
|
+
* @returns Extracted token object or undefined if invalid
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractPermalinkToken(permaLink: string): SaavnPermalinkToken | undefined;
|