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.
@@ -0,0 +1,39 @@
1
+ import type { RequestOptions, SourceInitData, SourceScriptMeta } from './types';
2
+ import type { HttpClient } from './http-client';
3
+ type Handler = (payload: unknown) => unknown | Promise<unknown>;
4
+ export declare const EVENT_NAMES: {
5
+ readonly request: "request";
6
+ readonly inited: "inited";
7
+ readonly updateAlert: "updateAlert";
8
+ };
9
+ export declare class LxRuntime {
10
+ readonly lx: {
11
+ version: string;
12
+ env: string;
13
+ currentScriptInfo: SourceScriptMeta;
14
+ EVENT_NAMES: typeof EVENT_NAMES;
15
+ request: (url: string, options: RequestOptions | ((err: Error | null, resp?: unknown) => void), callback?: (err: Error | null, resp?: unknown) => void) => void;
16
+ on: (eventName: string, handler: Handler) => void;
17
+ send: (eventName: string, data: unknown) => void;
18
+ utils: {
19
+ buffer: {
20
+ from: typeof Buffer.from;
21
+ toString: (buf: Buffer, encoding?: BufferEncoding) => string;
22
+ concat: typeof Buffer.concat;
23
+ };
24
+ crypto: {
25
+ md5: (str: string) => string;
26
+ hmac: (algorithm: string, key: string, data: string) => string;
27
+ };
28
+ };
29
+ };
30
+ private handlers;
31
+ private initedData;
32
+ private initedResolver;
33
+ private initedPromise;
34
+ constructor(httpClient: HttpClient);
35
+ dispatch(eventName: string, payload: unknown): Promise<unknown>;
36
+ getInitedData(): SourceInitData;
37
+ waitForInited(timeout: number): Promise<SourceInitData>;
38
+ }
39
+ export {};
@@ -0,0 +1,86 @@
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.LxRuntime = exports.EVENT_NAMES = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ exports.EVENT_NAMES = {
9
+ request: 'request',
10
+ inited: 'inited',
11
+ updateAlert: 'updateAlert',
12
+ };
13
+ class LxRuntime {
14
+ constructor(httpClient) {
15
+ this.handlers = {};
16
+ this.initedData = null;
17
+ this.initedResolver = null;
18
+ this.initedPromise = new Promise((resolve) => {
19
+ this.initedResolver = resolve;
20
+ });
21
+ this.lx = {
22
+ version: '1.1.0',
23
+ env: 'desktop',
24
+ currentScriptInfo: {},
25
+ EVENT_NAMES: exports.EVENT_NAMES,
26
+ request: (url, options, callback) => {
27
+ if (typeof options === 'function') {
28
+ httpClient.request(url, options);
29
+ return;
30
+ }
31
+ httpClient.request(url, options, callback);
32
+ },
33
+ on: (eventName, handler) => {
34
+ this.handlers[eventName] = handler;
35
+ },
36
+ send: (eventName, data) => {
37
+ if (eventName === exports.EVENT_NAMES.inited) {
38
+ this.initedData = data;
39
+ if (this.initedResolver)
40
+ this.initedResolver(this.initedData);
41
+ }
42
+ },
43
+ utils: {
44
+ buffer: {
45
+ from: Buffer.from,
46
+ toString: (buf, encoding) => buf.toString(encoding),
47
+ concat: Buffer.concat,
48
+ },
49
+ crypto: {
50
+ md5: (str) => {
51
+ return crypto_1.default.createHash('md5').update(str).digest('hex');
52
+ },
53
+ hmac: (algorithm, key, data) => {
54
+ return crypto_1.default.createHmac(algorithm, key).update(data).digest('hex');
55
+ },
56
+ },
57
+ },
58
+ };
59
+ }
60
+ async dispatch(eventName, payload) {
61
+ const handler = this.handlers[eventName];
62
+ if (!handler)
63
+ throw new Error(`[inki-music-api] 没有找到事件处理器: ${eventName}`);
64
+ return handler(payload);
65
+ }
66
+ getInitedData() {
67
+ return this.initedData;
68
+ }
69
+ waitForInited(timeout) {
70
+ if (this.initedData)
71
+ return Promise.resolve(this.initedData);
72
+ return new Promise((resolve, reject) => {
73
+ const timer = setTimeout(() => reject(new Error('[inki-music-api] 源脚本未调用 send(EVENT_NAMES.inited, ...),初始化失败')), timeout);
74
+ this.initedPromise
75
+ .then((data) => {
76
+ clearTimeout(timer);
77
+ resolve(data);
78
+ })
79
+ .catch((err) => {
80
+ clearTimeout(timer);
81
+ reject(err);
82
+ });
83
+ });
84
+ }
85
+ }
86
+ exports.LxRuntime = LxRuntime;
@@ -0,0 +1,10 @@
1
+ export declare class MemoryCache {
2
+ private store;
3
+ private maxEntries;
4
+ constructor(maxEntries: number);
5
+ setMaxEntries(maxEntries: number): void;
6
+ get<T>(key: string): T | null;
7
+ set(key: string, value: unknown, ttlMs: number): void;
8
+ clear(): void;
9
+ private prune;
10
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryCache = void 0;
4
+ class MemoryCache {
5
+ constructor(maxEntries) {
6
+ this.store = new Map();
7
+ this.maxEntries = maxEntries;
8
+ }
9
+ setMaxEntries(maxEntries) {
10
+ this.maxEntries = maxEntries;
11
+ this.prune();
12
+ }
13
+ get(key) {
14
+ const item = this.store.get(key);
15
+ if (!item)
16
+ return null;
17
+ if (Date.now() > item.expiresAt) {
18
+ this.store.delete(key);
19
+ return null;
20
+ }
21
+ return item.value;
22
+ }
23
+ set(key, value, ttlMs) {
24
+ this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
25
+ this.prune();
26
+ }
27
+ clear() {
28
+ this.store.clear();
29
+ }
30
+ prune() {
31
+ if (this.store.size <= this.maxEntries)
32
+ return;
33
+ const overflow = this.store.size - this.maxEntries;
34
+ const keys = this.store.keys();
35
+ for (let i = 0; i < overflow; i += 1) {
36
+ const next = keys.next();
37
+ if (next.done)
38
+ return;
39
+ this.store.delete(next.value);
40
+ }
41
+ }
42
+ }
43
+ exports.MemoryCache = MemoryCache;
@@ -0,0 +1,84 @@
1
+ import { SourceInstance } from './source-loader';
2
+ import type { CommentResult, MusicApiConfig, MusicInfo, RemoteSourceOptions, SearchOptions, SearchResult, SourceCapabilities, SongListDetailResult, SongListResult, SongListTagResult, SourceInfo, SourceKey, SupportedPlatform } from './types';
3
+ export declare class MusicApi {
4
+ private config;
5
+ private httpClient;
6
+ private sourceLoader;
7
+ private lxProjectApi;
8
+ private sourceInstance;
9
+ private cache;
10
+ constructor(config?: MusicApiConfig);
11
+ /** 更新运行时配置(超时、证书、重定向等) */
12
+ updateConfig(config: Partial<MusicApiConfig>): void;
13
+ /** 初始化:必须先加载 source,不再提供内置默认源 */
14
+ init(): Promise<SourceInstance>;
15
+ /** 从本地文件加载源脚本 */
16
+ loadSourceFromFile(scriptPath: string): Promise<SourceInstance>;
17
+ /** 从脚本文本加载源 */
18
+ loadSourceFromCode(code: string): Promise<SourceInstance>;
19
+ /** 从远程地址加载源(http/https) */
20
+ loadSourceFromRemote(url: string, options?: RemoteSourceOptions): Promise<SourceInstance>;
21
+ loadSourceFormRemote(url: string, options?: RemoteSourceOptions): Promise<SourceInstance>;
22
+ loadSourceFormRmote(url: string, options?: RemoteSourceOptions): Promise<SourceInstance>;
23
+ /** 手动设置已加载源实例 */
24
+ setSource(sourceInstance: SourceInstance): void;
25
+ /** 获取内置支持平台列表 */
26
+ getSupportedPlatforms(): SupportedPlatform[];
27
+ getCapabilities(): SourceCapabilities;
28
+ /** 获取当前源声明的可用源列表 */
29
+ getSupportedSourceList(): Promise<Array<{
30
+ key: SourceKey;
31
+ info: SourceInfo;
32
+ }>>;
33
+ private getSource;
34
+ /** 单平台搜索歌曲 */
35
+ search(source: string, keywords: string, options?: SearchOptions): Promise<SearchResult>;
36
+ /** 获取播放链接(仅此能力依赖 source 脚本) */
37
+ getMusicUrl(source: string, musicInfo: MusicInfo, quality?: string): Promise<unknown>;
38
+ /** 获取歌词 */
39
+ getLyric(source: string, musicInfo: MusicInfo): Promise<import("./types").MusicLyric>;
40
+ /** 获取封面 */
41
+ getPic(source: string, musicInfo: MusicInfo): Promise<string>;
42
+ /** 获取热门歌曲 */
43
+ getHot(source: string, options?: SearchOptions): Promise<import("./types").RankResult>;
44
+ /** 获取榜单歌曲 */
45
+ getRank(source: string, rankType: string, options?: SearchOptions): Promise<import("./types").RankResult>;
46
+ /** 获取歌单标签(含热门标签) */
47
+ getSongListTags(source: string): Promise<SongListTagResult>;
48
+ /** 获取歌单列表 */
49
+ getSongList(source: string, options?: SearchOptions & {
50
+ sortId?: string;
51
+ tagId?: string;
52
+ }): Promise<SongListResult>;
53
+ /** 获取歌单详情(含歌单内歌曲) */
54
+ getSongListDetail(source: string, listId: string, options?: SearchOptions): Promise<SongListDetailResult>;
55
+ /** 获取歌曲评论 */
56
+ getComment(source: string, musicInfo: MusicInfo, options?: SearchOptions): Promise<CommentResult>;
57
+ /** 获取热门评论 */
58
+ getHotComment(source: string, musicInfo: MusicInfo, options?: SearchOptions): Promise<CommentResult>;
59
+ /** 获取歌曲详情页地址 */
60
+ getMusicDetailPageUrl(source: string, musicInfo: MusicInfo): string | null;
61
+ /** 跨平台搜索同名歌曲(排除指定源) */
62
+ searchMusic(params: {
63
+ name: string;
64
+ singer?: string;
65
+ source?: SourceKey;
66
+ limit?: number;
67
+ }): Promise<SearchResult[]>;
68
+ /** 尝试从多平台匹配最接近的歌曲 */
69
+ findMusic(params: {
70
+ name: string;
71
+ singer?: string;
72
+ albumName?: string;
73
+ interval?: string;
74
+ source?: SourceKey;
75
+ }): Promise<MusicInfo[]>;
76
+ /** 透传调用源脚本的自定义 action */
77
+ dispatch(source: string, action: string, info?: Record<string, unknown>): Promise<unknown>;
78
+ getLoadedSource(): SourceInstance;
79
+ clearCache(): void;
80
+ private normalizeRetryConfig;
81
+ private normalizeCacheConfig;
82
+ private execute;
83
+ private executeCached;
84
+ }
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MusicApi = void 0;
4
+ const api_error_1 = require("./api-error");
5
+ const http_client_1 = require("./http-client");
6
+ const memory_cache_1 = require("./memory-cache");
7
+ const source_loader_1 = require("./source-loader");
8
+ const lx_project_api_1 = require("./lx-project-api");
9
+ const types_1 = require("./types");
10
+ const DEFAULT_CONFIG = {
11
+ initTimeout: 15000,
12
+ requestTimeout: 15000,
13
+ followMaxRedirects: 5,
14
+ rejectUnauthorized: false,
15
+ retry: {
16
+ retries: 2,
17
+ retryDelayMs: 200,
18
+ retryStatusCodes: [429, 500, 502, 503, 504],
19
+ retryErrorCodes: ['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EAI_AGAIN'],
20
+ },
21
+ cache: {
22
+ enabled: true,
23
+ ttlMs: 30 * 1000,
24
+ maxEntries: 1000,
25
+ },
26
+ };
27
+ class MusicApi {
28
+ constructor(config = {}) {
29
+ this.config = { ...DEFAULT_CONFIG, ...config };
30
+ this.httpClient = new http_client_1.HttpClient({
31
+ timeout: this.config.requestTimeout,
32
+ followMaxRedirects: this.config.followMaxRedirects,
33
+ rejectUnauthorized: this.config.rejectUnauthorized,
34
+ retry: this.normalizeRetryConfig(this.config.retry),
35
+ });
36
+ this.sourceLoader = new source_loader_1.SourceLoader(this.httpClient, { initTimeout: this.config.initTimeout });
37
+ this.lxProjectApi = new lx_project_api_1.LxProjectApi(this.httpClient);
38
+ this.sourceInstance = null;
39
+ this.cache = new memory_cache_1.MemoryCache(this.normalizeCacheConfig(this.config.cache).maxEntries);
40
+ }
41
+ /** 更新运行时配置(超时、证书、重定向等) */
42
+ updateConfig(config) {
43
+ this.config = { ...this.config, ...config };
44
+ const retry = this.normalizeRetryConfig(this.config.retry);
45
+ const cache = this.normalizeCacheConfig(this.config.cache);
46
+ this.httpClient.updateConfig({
47
+ timeout: this.config.requestTimeout,
48
+ followMaxRedirects: this.config.followMaxRedirects,
49
+ rejectUnauthorized: this.config.rejectUnauthorized,
50
+ retry,
51
+ });
52
+ this.sourceLoader.updateOptions({ initTimeout: this.config.initTimeout });
53
+ this.cache.setMaxEntries(cache.maxEntries);
54
+ }
55
+ /** 初始化:必须先加载 source,不再提供内置默认源 */
56
+ async init() {
57
+ if (this.sourceInstance)
58
+ return this.sourceInstance;
59
+ throw new api_error_1.ApiError('E_SOURCE_INIT', '[inki-music-api] source 未初始化,请先 loadSourceFromFile/loadSourceFromCode/loadSourceFromRemote');
60
+ }
61
+ /** 从本地文件加载源脚本 */
62
+ async loadSourceFromFile(scriptPath) {
63
+ this.sourceInstance = await this.sourceLoader.loadFromFile(scriptPath);
64
+ return this.sourceInstance;
65
+ }
66
+ /** 从脚本文本加载源 */
67
+ async loadSourceFromCode(code) {
68
+ this.sourceInstance = await this.sourceLoader.loadFromCode(code);
69
+ return this.sourceInstance;
70
+ }
71
+ /** 从远程地址加载源(http/https) */
72
+ async loadSourceFromRemote(url, options = {}) {
73
+ return this.execute('loadSourceFromRemote', async () => {
74
+ const { headers, timeout, rejectUnauthorized } = options;
75
+ const response = await this.httpClient.requestPromise(url, {
76
+ method: 'GET',
77
+ headers,
78
+ timeout,
79
+ rejectUnauthorized,
80
+ });
81
+ const body = response.body;
82
+ if (typeof body !== 'string') {
83
+ throw new api_error_1.ApiError('E_REMOTE_SOURCE_INVALID', '[inki-music-api] 远程源内容不是字符串脚本');
84
+ }
85
+ return this.loadSourceFromCode(body);
86
+ });
87
+ }
88
+ async loadSourceFormRemote(url, options = {}) {
89
+ return this.loadSourceFromRemote(url, options);
90
+ }
91
+ async loadSourceFormRmote(url, options = {}) {
92
+ return this.loadSourceFromRemote(url, options);
93
+ }
94
+ /** 手动设置已加载源实例 */
95
+ setSource(sourceInstance) {
96
+ this.sourceInstance = sourceInstance;
97
+ }
98
+ /** 获取内置支持平台列表 */
99
+ getSupportedPlatforms() {
100
+ return [...types_1.SUPPORTED_PLATFORMS];
101
+ }
102
+ getCapabilities() {
103
+ return {
104
+ search: true,
105
+ musicUrl: true,
106
+ lyric: true,
107
+ pic: true,
108
+ hot: true,
109
+ rank: true,
110
+ songListTags: true,
111
+ songList: true,
112
+ songListDetail: true,
113
+ comment: true,
114
+ hotComment: true,
115
+ searchMusic: true,
116
+ findMusic: true,
117
+ dispatch: true,
118
+ };
119
+ }
120
+ /** 获取当前源声明的可用源列表 */
121
+ async getSupportedSourceList() {
122
+ return this.execute('getSupportedSourceList', async () => {
123
+ const source = await this.getSource();
124
+ return Object.entries(source.sources).map(([key, info]) => ({ key, info }));
125
+ });
126
+ }
127
+ async getSource() {
128
+ if (this.sourceInstance)
129
+ return this.sourceInstance;
130
+ return this.init();
131
+ }
132
+ /** 单平台搜索歌曲 */
133
+ async search(source, keywords, options = {}) {
134
+ return this.executeCached(`search:${source}:${keywords}:${options.page || 1}:${options.limit || 20}`, async () => {
135
+ const { page = 1, limit = 20 } = options;
136
+ return this.lxProjectApi.search(source, keywords, page, limit);
137
+ });
138
+ }
139
+ /** 获取播放链接(仅此能力依赖 source 脚本) */
140
+ async getMusicUrl(source, musicInfo, quality = '128k') {
141
+ return this.execute('getMusicUrl', async () => {
142
+ const sourceInstance = await this.getSource();
143
+ return sourceInstance.getMusicUrl(source, musicInfo, quality);
144
+ }, source);
145
+ }
146
+ /** 获取歌词 */
147
+ async getLyric(source, musicInfo) {
148
+ return this.executeCached(`lyric:${source}:${musicInfo.songmid}`, async () => this.lxProjectApi.lyric(source, musicInfo), source);
149
+ }
150
+ /** 获取封面 */
151
+ async getPic(source, musicInfo) {
152
+ return this.executeCached(`pic:${source}:${musicInfo.songmid}`, async () => this.lxProjectApi.pic(source, musicInfo), source);
153
+ }
154
+ /** 获取热门歌曲 */
155
+ async getHot(source, options = {}) {
156
+ return this.executeCached(`hot:${source}:${options.page || 1}:${options.limit || 20}`, async () => {
157
+ const { page = 1, limit = 20 } = options;
158
+ return this.lxProjectApi.hot(source, page, limit);
159
+ }, source);
160
+ }
161
+ /** 获取榜单歌曲 */
162
+ async getRank(source, rankType, options = {}) {
163
+ return this.executeCached(`rank:${source}:${rankType}:${options.page || 1}:${options.limit || 20}`, async () => {
164
+ const { page = 1, limit = 20 } = options;
165
+ return this.lxProjectApi.rank(source, rankType, page, limit);
166
+ }, source);
167
+ }
168
+ /** 获取歌单标签(含热门标签) */
169
+ async getSongListTags(source) {
170
+ return this.executeCached(`song-list-tags:${source}`, async () => this.lxProjectApi.songListTags(source), source);
171
+ }
172
+ /** 获取歌单列表 */
173
+ async getSongList(source, options = {}) {
174
+ return this.executeCached(`song-list:${source}:${options.sortId || 'hot'}:${options.tagId || ''}:${options.page || 1}:${options.limit || 30}`, async () => {
175
+ const { page = 1, limit = 30, sortId = 'hot', tagId = '' } = options;
176
+ return this.lxProjectApi.songList(source, sortId, tagId, page, limit);
177
+ }, source);
178
+ }
179
+ /** 获取歌单详情(含歌单内歌曲) */
180
+ async getSongListDetail(source, listId, options = {}) {
181
+ return this.executeCached(`song-list-detail:${source}:${listId}:${options.page || 1}:${options.limit || 100}`, async () => {
182
+ const { page = 1, limit = 100 } = options;
183
+ return this.lxProjectApi.songListDetail(source, listId, page, limit);
184
+ }, source);
185
+ }
186
+ /** 获取歌曲评论 */
187
+ async getComment(source, musicInfo, options = {}) {
188
+ return this.executeCached(`comment:${source}:${musicInfo.songmid}:${options.page || 1}:${options.limit || 20}`, async () => {
189
+ const { page = 1, limit = 20 } = options;
190
+ return this.lxProjectApi.comment(source, musicInfo, page, limit, false);
191
+ }, source);
192
+ }
193
+ /** 获取热门评论 */
194
+ async getHotComment(source, musicInfo, options = {}) {
195
+ return this.executeCached(`hot-comment:${source}:${musicInfo.songmid}:${options.page || 1}:${options.limit || 20}`, async () => {
196
+ const { page = 1, limit = 20 } = options;
197
+ return this.lxProjectApi.comment(source, musicInfo, page, limit, true);
198
+ }, source);
199
+ }
200
+ /** 获取歌曲详情页地址 */
201
+ getMusicDetailPageUrl(source, musicInfo) {
202
+ return this.lxProjectApi.getMusicDetailPageUrl(source, musicInfo);
203
+ }
204
+ /** 跨平台搜索同名歌曲(排除指定源) */
205
+ async searchMusic(params) {
206
+ return this.execute('searchMusic', async () => {
207
+ const { name, singer = '', source, limit = 25 } = params;
208
+ const keyword = `${name} ${singer}`.trim();
209
+ const tasks = this.getSupportedPlatforms()
210
+ .filter((platform) => platform !== source)
211
+ .map((platform) => this.search(platform, keyword, { page: 1, limit }).catch(() => null));
212
+ const result = await Promise.all(tasks);
213
+ return result.filter((item) => item != null);
214
+ });
215
+ }
216
+ /** 尝试从多平台匹配最接近的歌曲 */
217
+ async findMusic(params) {
218
+ return this.execute('findMusic', async () => {
219
+ const { name, singer = '', albumName = '', interval = '', source } = params;
220
+ const lists = await this.searchMusic({ name, singer, source, limit: 25 });
221
+ const normalize = (text) => text.toLowerCase().replace(/\s|'|\.|,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[|!|!/g, '');
222
+ const targetName = normalize(name);
223
+ const targetSinger = normalize(singer);
224
+ const targetAlbum = normalize(albumName);
225
+ const targetInterval = interval;
226
+ const candidates = [];
227
+ for (const list of lists) {
228
+ for (const item of list.list) {
229
+ let score = 0;
230
+ if (normalize(item.name) === targetName)
231
+ score += 4;
232
+ if (targetSinger && normalize(item.singer) === targetSinger)
233
+ score += 3;
234
+ if (targetAlbum && normalize(item.albumName) === targetAlbum)
235
+ score += 2;
236
+ if (targetInterval && item.interval === targetInterval)
237
+ score += 2;
238
+ if (score > 0)
239
+ candidates.push({ score, item });
240
+ }
241
+ }
242
+ return candidates.sort((a, b) => b.score - a.score).map((item) => item.item);
243
+ });
244
+ }
245
+ /** 透传调用源脚本的自定义 action */
246
+ async dispatch(source, action, info = {}) {
247
+ return this.execute('dispatch', async () => {
248
+ const sourceInstance = await this.getSource();
249
+ return sourceInstance.dispatchAction(source, action, info);
250
+ }, source);
251
+ }
252
+ getLoadedSource() {
253
+ return this.sourceInstance;
254
+ }
255
+ clearCache() {
256
+ this.cache.clear();
257
+ }
258
+ normalizeRetryConfig(config) {
259
+ return {
260
+ retries: config?.retries ?? DEFAULT_CONFIG.retry.retries,
261
+ retryDelayMs: config?.retryDelayMs ?? DEFAULT_CONFIG.retry.retryDelayMs,
262
+ retryStatusCodes: config?.retryStatusCodes ?? DEFAULT_CONFIG.retry.retryStatusCodes,
263
+ retryErrorCodes: config?.retryErrorCodes ?? DEFAULT_CONFIG.retry.retryErrorCodes,
264
+ };
265
+ }
266
+ normalizeCacheConfig(config) {
267
+ return {
268
+ enabled: config?.enabled ?? DEFAULT_CONFIG.cache.enabled,
269
+ ttlMs: config?.ttlMs ?? DEFAULT_CONFIG.cache.ttlMs,
270
+ maxEntries: config?.maxEntries ?? DEFAULT_CONFIG.cache.maxEntries,
271
+ };
272
+ }
273
+ async execute(operation, runner, source) {
274
+ try {
275
+ return await runner();
276
+ }
277
+ catch (error) {
278
+ throw (0, api_error_1.toApiError)(error, 'E_UPSTREAM', `[inki-music-api] ${operation} failed`, source);
279
+ }
280
+ }
281
+ async executeCached(key, runner, source) {
282
+ const cache = this.normalizeCacheConfig(this.config.cache);
283
+ if (cache.enabled) {
284
+ const existed = this.cache.get(key);
285
+ if (existed != null)
286
+ return existed;
287
+ }
288
+ const result = await this.execute(key, runner, source);
289
+ if (cache.enabled)
290
+ this.cache.set(key, result, cache.ttlMs);
291
+ return result;
292
+ }
293
+ }
294
+ exports.MusicApi = MusicApi;
@@ -0,0 +1,28 @@
1
+ import { LxRuntime } from './lx-runtime';
2
+ import type { HttpClient } from './http-client';
3
+ import type { SourceInfo, SourceInitData, SourceScriptMeta } from './types';
4
+ export declare class SourceInstance {
5
+ readonly meta: SourceScriptMeta;
6
+ readonly sources: Record<string, SourceInfo>;
7
+ private runtime;
8
+ constructor(runtime: LxRuntime, initedData: SourceInitData, meta: SourceScriptMeta);
9
+ private dispatch;
10
+ getMusicUrl(source: string, musicInfo: unknown, quality?: string): Promise<unknown>;
11
+ getLyric(source: string, musicInfo: unknown): Promise<unknown>;
12
+ getPic(source: string, musicInfo: unknown): Promise<unknown>;
13
+ search(source: string, keywords: string, page?: number, limit?: number): Promise<unknown>;
14
+ getHot(source: string, page?: number, limit?: number): Promise<unknown>;
15
+ getRank(source: string, rankType: string, page?: number, limit?: number): Promise<unknown>;
16
+ dispatchAction(source: string, action: string, info: Record<string, unknown>): Promise<unknown>;
17
+ }
18
+ export interface LoaderOptions {
19
+ initTimeout: number;
20
+ }
21
+ export declare class SourceLoader {
22
+ private httpClient;
23
+ private options;
24
+ constructor(httpClient: HttpClient, options: LoaderOptions);
25
+ updateOptions(options: Partial<LoaderOptions>): void;
26
+ loadFromFile(scriptPath: string): Promise<SourceInstance>;
27
+ loadFromCode(code: string): Promise<SourceInstance>;
28
+ }