inki-music-api 2.0.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/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # inki-music-api
2
+
3
+ `inki-music-api` 是基于 TypeScript + axios 的 lx-music 自定义源封装包,核心导出 `MusicApi` class,支持:
4
+
5
+ - 统一配置化初始化
6
+ - 必须显式加载 source(本地/远程/代码)
7
+ - 外部源文件/代码动态加载
8
+ - `musicUrl` 走源脚本,其它接口走内置聚合实现
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ npm install
14
+ ```
15
+
16
+ ## 快速开始
17
+
18
+ ```js
19
+ const { MusicApi } = require('inki-music-api');
20
+
21
+ async function main() {
22
+ const api = new MusicApi();
23
+ await api.loadSourceFromFile('./your-source.js');
24
+
25
+ const result = await api.search('kw', '周杰伦', { page: 1, limit: 5 });
26
+ const first = result.list[0];
27
+
28
+ const url = await api.getMusicUrl('kw', first, '128k');
29
+ const lyric = await api.getLyric('kw', first);
30
+ const pic = await api.getPic('kw', first);
31
+ const hot = await api.getHot('kw', { page: 1, limit: 5 });
32
+ const rank = await api.getRank('kw', 'hot', { page: 1, limit: 5 });
33
+
34
+ console.log(url, lyric, pic, hot.total, rank.total);
35
+ }
36
+
37
+ main();
38
+ ```
39
+
40
+ ## 配置项
41
+
42
+ ```ts
43
+ type MusicApiConfig = {
44
+ initTimeout?: number;
45
+ requestTimeout?: number;
46
+ followMaxRedirects?: number;
47
+ rejectUnauthorized?: boolean;
48
+ retry?: {
49
+ retries?: number;
50
+ retryDelayMs?: number;
51
+ retryStatusCodes?: number[];
52
+ retryErrorCodes?: string[];
53
+ };
54
+ cache?: {
55
+ enabled?: boolean;
56
+ ttlMs?: number;
57
+ maxEntries?: number;
58
+ };
59
+ };
60
+ ```
61
+
62
+ 默认配置:
63
+
64
+ ```js
65
+ {
66
+ initTimeout: 15000,
67
+ requestTimeout: 15000,
68
+ followMaxRedirects: 5,
69
+ rejectUnauthorized: false,
70
+ retry: {
71
+ retries: 2,
72
+ retryDelayMs: 200,
73
+ retryStatusCodes: [429, 500, 502, 503, 504],
74
+ retryErrorCodes: ['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EAI_AGAIN']
75
+ },
76
+ cache: {
77
+ enabled: true,
78
+ ttlMs: 30000,
79
+ maxEntries: 1000
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## 加载外部源
85
+
86
+ ```js
87
+ const api = new MusicApi();
88
+
89
+ await api.loadSourceFromFile('./my-source.js');
90
+ // 或
91
+ await api.loadSourceFromCode(sourceCode);
92
+ ```
93
+
94
+ ## API
95
+
96
+ - `init()`
97
+ - `updateConfig(partialConfig)`
98
+ - `loadSourceFromFile(filePath)`
99
+ - `loadSourceFromCode(code)`
100
+ - `loadSourceFromRemote(url, options)`
101
+ - `loadSourceFormRemote(url, options)`
102
+ - `loadSourceFormRmote(url, options)`
103
+ - `setSource(sourceInstance)`
104
+ - `getSupportedPlatforms()`
105
+ - `getCapabilities(source?)`
106
+ - `getSupportedSourceList()`
107
+ - `search(source, keywords, { page, limit })`
108
+ - `searchMusic({ name, singer, source, limit })`
109
+ - `findMusic({ name, singer, albumName, interval, source })`
110
+ - `getMusicUrl(source, musicInfo, quality)`
111
+ - `getLyric(source, musicInfo)`
112
+ - `getPic(source, musicInfo)`
113
+ - `getHot(source, { page, limit })`
114
+ - `getRank(source, rankType, { page, limit })`
115
+ - `getComment(source, musicInfo, { page, limit })`
116
+ - `getHotComment(source, musicInfo, { page, limit })`
117
+ - `getMusicDetailPageUrl(source, musicInfo)`
118
+ - `getSongListTags(source)`
119
+ - `getSongList(source, { sortId, tagId, page, limit })`
120
+ - `getSongListDetail(source, listId, { page, limit })`
121
+ - `dispatch(source, action, info)`
122
+ - `clearCache()`
123
+
124
+ ## 开发命令
125
+
126
+ ```bash
127
+ npm run build
128
+ npm run typecheck
129
+ npm run lint
130
+ npm run format:check
131
+ npm test
132
+ npm publish
133
+ ```
@@ -0,0 +1,13 @@
1
+ import type { ApiErrorCode } from './types';
2
+ export declare class ApiError extends Error {
3
+ code: ApiErrorCode;
4
+ statusCode?: number;
5
+ source?: string;
6
+ cause?: unknown;
7
+ constructor(code: ApiErrorCode, message: string, options?: {
8
+ statusCode?: number;
9
+ source?: string;
10
+ cause?: unknown;
11
+ });
12
+ }
13
+ export declare const toApiError: (error: unknown, fallbackCode: ApiErrorCode, fallbackMessage: string, source?: string) => ApiError;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toApiError = exports.ApiError = void 0;
4
+ class ApiError extends Error {
5
+ constructor(code, message, options = {}) {
6
+ super(message);
7
+ this.name = 'ApiError';
8
+ this.code = code;
9
+ this.statusCode = options.statusCode;
10
+ this.source = options.source;
11
+ this.cause = options.cause;
12
+ }
13
+ }
14
+ exports.ApiError = ApiError;
15
+ const toApiError = (error, fallbackCode, fallbackMessage, source) => {
16
+ if (error instanceof ApiError)
17
+ return error;
18
+ if (error instanceof Error) {
19
+ return new ApiError(fallbackCode, error.message || fallbackMessage, {
20
+ source,
21
+ cause: error,
22
+ });
23
+ }
24
+ return new ApiError(fallbackCode, fallbackMessage, {
25
+ source,
26
+ cause: error,
27
+ });
28
+ };
29
+ exports.toApiError = toApiError;
@@ -0,0 +1,16 @@
1
+ import type { RequestOptions, RequestResponse, RetryConfig } from './types';
2
+ export type RequestCallback = (err: Error | null, resp?: RequestResponse) => void;
3
+ export interface HttpClientConfig {
4
+ timeout: number;
5
+ followMaxRedirects: number;
6
+ rejectUnauthorized: boolean;
7
+ retry: Required<RetryConfig>;
8
+ }
9
+ export declare class HttpClient {
10
+ private config;
11
+ constructor(config: HttpClientConfig);
12
+ updateConfig(config: Partial<HttpClientConfig>): void;
13
+ request(url: string, options: RequestOptions | RequestCallback, callback?: RequestCallback): void;
14
+ requestPromise<T = unknown>(url: string, options?: RequestOptions): Promise<RequestResponse<T>>;
15
+ private wait;
16
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpClient = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const http_1 = require("http");
9
+ const https_1 = require("https");
10
+ const url_1 = require("url");
11
+ const api_error_1 = require("./api-error");
12
+ class HttpClient {
13
+ constructor(config) {
14
+ this.config = config;
15
+ }
16
+ updateConfig(config) {
17
+ this.config = { ...this.config, ...config };
18
+ }
19
+ request(url, options, callback) {
20
+ let requestOptions = {};
21
+ let cb;
22
+ if (typeof options === 'function') {
23
+ cb = options;
24
+ }
25
+ else {
26
+ requestOptions = options;
27
+ cb = callback;
28
+ }
29
+ this.requestPromise(url, requestOptions).then((resp) => cb(null, resp), (err) => cb(err));
30
+ }
31
+ async requestPromise(url, options = {}) {
32
+ const { method = 'GET', headers = {}, body, form, timeout = this.config.timeout, follow_max = this.config.followMaxRedirects, rejectUnauthorized = this.config.rejectUnauthorized, } = options;
33
+ const requestConfig = {
34
+ url,
35
+ method: method.toUpperCase(),
36
+ headers,
37
+ timeout,
38
+ maxRedirects: follow_max,
39
+ validateStatus: () => true,
40
+ responseType: 'text',
41
+ httpAgent: new http_1.Agent({ keepAlive: true }),
42
+ httpsAgent: new https_1.Agent({ keepAlive: true, rejectUnauthorized }),
43
+ };
44
+ if (form) {
45
+ const params = new url_1.URLSearchParams();
46
+ for (const [key, value] of Object.entries(form))
47
+ params.append(key, String(value));
48
+ requestConfig.data = params.toString();
49
+ requestConfig.headers = {
50
+ ...headers,
51
+ 'Content-Type': 'application/x-www-form-urlencoded',
52
+ };
53
+ }
54
+ else if (body != null) {
55
+ requestConfig.data = body;
56
+ requestConfig.headers = {
57
+ ...headers,
58
+ 'Content-Type': headers['Content-Type'] || 'application/json',
59
+ };
60
+ }
61
+ const maxRetry = Math.max(0, this.config.retry.retries);
62
+ const retryStatusCodes = new Set(this.config.retry.retryStatusCodes);
63
+ const retryErrorCodes = new Set(this.config.retry.retryErrorCodes);
64
+ const baseDelay = Math.max(0, this.config.retry.retryDelayMs);
65
+ for (let attempt = 0; attempt <= maxRetry; attempt += 1) {
66
+ try {
67
+ const result = await axios_1.default.request(requestConfig);
68
+ if (attempt < maxRetry && retryStatusCodes.has(result.status)) {
69
+ await this.wait(baseDelay * (attempt + 1));
70
+ continue;
71
+ }
72
+ const raw = result.data ?? '';
73
+ let parsed;
74
+ try {
75
+ parsed = JSON.parse(raw);
76
+ }
77
+ catch {
78
+ parsed = raw;
79
+ }
80
+ return {
81
+ statusCode: result.status,
82
+ headers: result.headers,
83
+ body: parsed,
84
+ };
85
+ }
86
+ catch (error) {
87
+ const err = error;
88
+ if (attempt < maxRetry && err.code && retryErrorCodes.has(err.code)) {
89
+ await this.wait(baseDelay * (attempt + 1));
90
+ continue;
91
+ }
92
+ if (err.code === 'ECONNABORTED') {
93
+ throw new api_error_1.ApiError('E_TIMEOUT', err.message || 'request timeout', {
94
+ cause: err,
95
+ });
96
+ }
97
+ throw new api_error_1.ApiError('E_REQUEST_FAILED', err.message || 'request failed', {
98
+ cause: err,
99
+ });
100
+ }
101
+ }
102
+ throw new api_error_1.ApiError('E_REQUEST_FAILED', 'request failed');
103
+ }
104
+ async wait(ms) {
105
+ if (!ms)
106
+ return;
107
+ await new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
109
+ }
110
+ exports.HttpClient = HttpClient;
@@ -0,0 +1,5 @@
1
+ export { ApiError } from './api-error';
2
+ export { MusicApi } from './music-api';
3
+ export { SourceLoader, SourceInstance } from './source-loader';
4
+ export { SUPPORTED_PLATFORMS } from './types';
5
+ export type { ApiErrorCode, CacheConfig, CommentItem, CommentResult, MusicApiConfig, MusicInfo, MusicLyric, MusicQuality, MusicTypeInfo, RankResult, RemoteSourceOptions, RetryConfig, SearchOptions, SearchResult, SongListDetailInfo, SongListDetailResult, SongListItem, SongListResult, SongListTag, SongListTagGroup, SongListTagResult, SourceCapabilities, SourceInfo, SourceKey, SupportedPlatform, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SUPPORTED_PLATFORMS = exports.SourceInstance = exports.SourceLoader = exports.MusicApi = exports.ApiError = void 0;
4
+ var api_error_1 = require("./api-error");
5
+ Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return api_error_1.ApiError; } });
6
+ var music_api_1 = require("./music-api");
7
+ Object.defineProperty(exports, "MusicApi", { enumerable: true, get: function () { return music_api_1.MusicApi; } });
8
+ var source_loader_1 = require("./source-loader");
9
+ Object.defineProperty(exports, "SourceLoader", { enumerable: true, get: function () { return source_loader_1.SourceLoader; } });
10
+ Object.defineProperty(exports, "SourceInstance", { enumerable: true, get: function () { return source_loader_1.SourceInstance; } });
11
+ var types_1 = require("./types");
12
+ Object.defineProperty(exports, "SUPPORTED_PLATFORMS", { enumerable: true, get: function () { return types_1.SUPPORTED_PLATFORMS; } });
@@ -0,0 +1,17 @@
1
+ import type { HttpClient } from './http-client';
2
+ import type { CommentResult, MusicInfo, MusicLyric, RankResult, SearchResult, SongListDetailResult, SongListResult, SongListTagResult, SourceKey } from './types';
3
+ export declare class LxProjectApi {
4
+ private httpClient;
5
+ constructor(httpClient: HttpClient);
6
+ private http;
7
+ search(source: SourceKey, keywords: string, page?: number, limit?: number): Promise<SearchResult>;
8
+ hot(source: SourceKey, page?: number, limit?: number): Promise<RankResult>;
9
+ rank(source: SourceKey, rankType: string, page?: number, limit?: number): Promise<RankResult>;
10
+ lyric(source: SourceKey, musicInfo: MusicInfo): Promise<MusicLyric>;
11
+ pic(source: SourceKey, musicInfo: MusicInfo): Promise<string | null>;
12
+ comment(source: SourceKey, musicInfo: MusicInfo, page?: number, limit?: number, hot?: boolean): Promise<CommentResult>;
13
+ getMusicDetailPageUrl(source: SourceKey, musicInfo: MusicInfo): string | null;
14
+ songListTags(source: SourceKey): Promise<SongListTagResult>;
15
+ songList(source: SourceKey, sortId?: string, tagId?: string, page?: number, limit?: number): Promise<SongListResult>;
16
+ songListDetail(source: SourceKey, listId: string, page?: number, limit?: number): Promise<SongListDetailResult>;
17
+ }