@doitian/dsh-music 0.1.1 → 0.1.4
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 +223 -43
- package/cordis.patch.yml +14 -0
- package/lib/client.js +48 -11
- package/lib/dj.js +234 -48
- package/lib/index.js +57 -3
- package/lib/likes.js +169 -0
- package/lib/netease.js +120 -7
- package/lib/panel.html +749 -235
- package/lib/router.js +210 -22
- package/lib/session.js +93 -19
- package/lib/state.js +15 -0
- package/package.json +5 -3
package/lib/netease.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* - `api/toplist` anonymous OK
|
|
15
15
|
* - `api/discovery/simiSong` anonymous OK
|
|
16
16
|
* - `api/personalized/newsong` anonymous OK
|
|
17
|
+
* - `api/song/like` POST: like / un-like, needs a session
|
|
18
|
+
* - `api/song/like/check` which of these ids the account liked
|
|
17
19
|
* - `api/login/qrcode/*` QR login handshake
|
|
18
20
|
*
|
|
19
21
|
* @module dsh-music/netease
|
|
@@ -294,21 +296,37 @@ export class Netease {
|
|
|
294
296
|
|
|
295
297
|
/**
|
|
296
298
|
* Raw streaming fetch, used by the audio proxy. The caller owns the body.
|
|
299
|
+
*
|
|
300
|
+
* The deadline covers only the connect and the response headers. The body
|
|
301
|
+
* then streams for as long as the browser keeps reading — potentially the
|
|
302
|
+
* whole track, because the element pauses and resumes its reads as its
|
|
303
|
+
* buffer fills and drains. A whole-request timeout here (`AbortSignal`
|
|
304
|
+
* watches the body too) used to cut every stream still open at that point;
|
|
305
|
+
* the panel read the truncation as a failed track and skipped mid-song. A
|
|
306
|
+
* body that stops flowing is still bounded by undici's own idle timeout.
|
|
307
|
+
*
|
|
297
308
|
* @returns {Promise<Response>}
|
|
298
309
|
*/
|
|
299
|
-
async fetchAudio(url, { range, method = 'GET' } = {}) {
|
|
310
|
+
async fetchAudio(url, { range, method = 'GET', headerTimeoutMs = 30_000 } = {}) {
|
|
300
311
|
const headers = {
|
|
301
312
|
Referer: `${API_ORIGIN}/`,
|
|
302
313
|
'User-Agent': UA,
|
|
303
314
|
Cookie: this.jar.header(),
|
|
304
315
|
};
|
|
305
316
|
if (range) headers.Range = range;
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
317
|
+
const controller = new AbortController();
|
|
318
|
+
const deadline = setTimeout(() => controller.abort(), headerTimeoutMs);
|
|
319
|
+
let response;
|
|
320
|
+
try {
|
|
321
|
+
response = await fetch(url, {
|
|
322
|
+
method,
|
|
323
|
+
headers,
|
|
324
|
+
redirect: 'follow',
|
|
325
|
+
signal: controller.signal,
|
|
326
|
+
});
|
|
327
|
+
} finally {
|
|
328
|
+
clearTimeout(deadline);
|
|
329
|
+
}
|
|
312
330
|
this.jar.absorb(response);
|
|
313
331
|
return response;
|
|
314
332
|
}
|
|
@@ -528,6 +546,78 @@ export class Netease {
|
|
|
528
546
|
return normalizeTracks(data?.data);
|
|
529
547
|
}
|
|
530
548
|
|
|
549
|
+
// ------------------------------------------------------------------ likes
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Like or un-like one track on the account.
|
|
553
|
+
*
|
|
554
|
+
* `/api/song/like` is one of the few *write* endpoints the plain web API
|
|
555
|
+
* still serves unencrypted — it answers `{playlistId, code: 200}` and puts the
|
|
556
|
+
* track in (or takes it out of) the account's 我喜欢的音乐 playlist. It takes
|
|
557
|
+
* the direction rather than toggling, so the caller must know which one it
|
|
558
|
+
* means; ask {@link likedIds} when it does not.
|
|
559
|
+
*
|
|
560
|
+
* A refusal is reported, not thrown: an anonymous session answers `code: 301`
|
|
561
|
+
* and a delisted track answers `code: 400` with NetEase's own reason
|
|
562
|
+
* ("歌曲已经下架了"), and both are ordinary outcomes of a click. Only a
|
|
563
|
+
* transport failure throws.
|
|
564
|
+
*
|
|
565
|
+
* @param {number|string} id track id.
|
|
566
|
+
* @param {boolean} like `true` to like, `false` to remove the like.
|
|
567
|
+
*/
|
|
568
|
+
async likeSong(id, like = true) {
|
|
569
|
+
const trackId = Number(id);
|
|
570
|
+
if (!Number.isFinite(trackId)) {
|
|
571
|
+
return { ok: false, id, code: 'BAD_ID', reason: `invalid track id "${id}"` };
|
|
572
|
+
}
|
|
573
|
+
const data = await this.call('/api/song/like', {
|
|
574
|
+
method: 'POST',
|
|
575
|
+
query: { trackId, like: Boolean(like) },
|
|
576
|
+
});
|
|
577
|
+
if (Number(data?.code) !== 200) {
|
|
578
|
+
return {
|
|
579
|
+
ok: false,
|
|
580
|
+
id: trackId,
|
|
581
|
+
code: data?.code ?? 'UNKNOWN',
|
|
582
|
+
reason:
|
|
583
|
+
data?.message ||
|
|
584
|
+
data?.msg ||
|
|
585
|
+
(this.authenticated ? 'NetEase refused the change' : 'sign in to NetEase first'),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return { ok: true, id: trackId, liked: Boolean(like), playlistId: data?.playlistId ?? null };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Which of these tracks the account has liked.
|
|
593
|
+
*
|
|
594
|
+
* `/api/song/like/check` answers `{ids: [...the liked subset...]}`, so one
|
|
595
|
+
* request covers a whole queue. An anonymous session answers `code: 301` for
|
|
596
|
+
* every id — a refusal, not "nothing is liked" — and comes back as
|
|
597
|
+
* `ok: false` so a caller never caches it as an answer.
|
|
598
|
+
*
|
|
599
|
+
* @param {Iterable<number|string>} ids
|
|
600
|
+
* @returns {Promise<{ok: boolean, ids: number[], code?: number|string, reason?: string}>}
|
|
601
|
+
*/
|
|
602
|
+
async likedIds(ids) {
|
|
603
|
+
const list = [...new Set([...ids].map(Number).filter(Number.isFinite))];
|
|
604
|
+
// NetEase answers an empty batch with `code: 400`, and an empty question has
|
|
605
|
+
// no answer to fetch anyway.
|
|
606
|
+
if (list.length === 0) return { ok: true, ids: [] };
|
|
607
|
+
const data = await this.call('/api/song/like/check', {
|
|
608
|
+
query: { trackIds: JSON.stringify(list) },
|
|
609
|
+
});
|
|
610
|
+
if (Number(data?.code) !== 200 || !Array.isArray(data?.ids)) {
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
ids: [],
|
|
614
|
+
code: data?.code ?? 'UNKNOWN',
|
|
615
|
+
reason: data?.message || data?.msg || 'NetEase refused the like check',
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
return { ok: true, ids: data.ids.map(Number) };
|
|
619
|
+
}
|
|
620
|
+
|
|
531
621
|
// -------------------------------------------------------------- account
|
|
532
622
|
|
|
533
623
|
/** Account summary; `null` when the session is anonymous or expired. */
|
|
@@ -561,9 +651,32 @@ export class Netease {
|
|
|
561
651
|
cover: resizeImage(item.coverImgUrl, 300),
|
|
562
652
|
trackCount: item.trackCount ?? 0,
|
|
563
653
|
subscribed: Boolean(item.subscribed),
|
|
654
|
+
/** `5` marks 我喜欢的音乐 — the account's likes, as a playlist. */
|
|
655
|
+
specialType: item.specialType ?? 0,
|
|
564
656
|
}));
|
|
565
657
|
}
|
|
566
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Every track id in the account's 我喜欢的音乐 playlist, or `null` when there
|
|
661
|
+
* is no account or no such playlist.
|
|
662
|
+
*
|
|
663
|
+
* This is the whole like state in one read, which is what makes it useful for
|
|
664
|
+
* reconciling a local mirror; use {@link likedIds} to ask about specific
|
|
665
|
+
* tracks. The playlist's length is not capped by `n` — NetEase returns the
|
|
666
|
+
* complete `trackIds` list — so one request covers a library of any size.
|
|
667
|
+
*
|
|
668
|
+
* @param {number|string} [uid] owner; defaults to the fetched account.
|
|
669
|
+
*/
|
|
670
|
+
async likedPlaylistIds(uid) {
|
|
671
|
+
const owner = Number(uid ?? this.account?.uid);
|
|
672
|
+
if (!Number.isFinite(owner)) return null;
|
|
673
|
+
const playlists = await this.userPlaylists(owner, { limit: 1000 });
|
|
674
|
+
const liked = playlists.find((playlist) => playlist.specialType === 5);
|
|
675
|
+
if (!liked) return null;
|
|
676
|
+
const detail = await this.call('/api/v6/playlist/detail', { query: { id: liked.id, n: 1 } });
|
|
677
|
+
return (detail?.playlist?.trackIds ?? []).map((entry) => entry.id).filter(Number.isFinite);
|
|
678
|
+
}
|
|
679
|
+
|
|
567
680
|
// ------------------------------------------------------------ QR login
|
|
568
681
|
|
|
569
682
|
/**
|