Skip to content

Commit eacfc56

Browse files
authored
Merge pull request #135 from smzase/dev-branch
fix: 修复网易云听歌打卡上报参数
2 parents 67169d2 + cd886b9 commit eacfc56

15 files changed

Lines changed: 699 additions & 240 deletions

File tree

electron/main/apis/netease/core/ncbl.ts

Lines changed: 34 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { randomBytes, randomUUID as cryptoRandomUUID } from "node:crypto";
22
import * as zlib from "node:zlib";
33
import { CLIENT_LOG3_DOMAIN } from "./config";
4+
import { createPlaybackLogContext } from "./playLog";
45
import { fetchWithProxy } from "@main/utils/proxy";
56

67
interface NeteaseLogRecord {
@@ -43,22 +44,6 @@ interface NcblContext {
4344
processId: number;
4445
}
4546

46-
interface NcblSong {
47-
id: number;
48-
name: string;
49-
artist: string;
50-
bitrate: number;
51-
level: string;
52-
vip: boolean;
53-
time: number;
54-
}
55-
56-
interface NcblSource {
57-
id: string;
58-
type: string;
59-
name: string;
60-
}
61-
6247
interface UploadResult {
6348
success: boolean;
6449
fileName: string;
@@ -224,144 +209,43 @@ const buildRecord = ({ time, action, data }: NeteaseLogRecord): string => {
224209
export const buildRecords = (records: NeteaseLogRecord[]): string =>
225210
records.map(buildRecord).join("");
226211

227-
export const buildPlv = (
228-
ctx: NcblContext,
229-
song: NcblSong,
230-
source: NcblSource,
231-
): Record<string, unknown> => {
232-
const now = Date.now();
233-
return {
234-
mode: "circulation",
235-
download: 0,
236-
alg: "",
237-
status: "front",
238-
id: String(song.id),
239-
bitrate: song.bitrate,
240-
type: "song",
241-
is_listentogether: 0,
242-
source: source.name,
243-
is_heart: 0,
244-
resource_ratio: "",
245-
resource_time: song.time,
246-
musiceffect_id: "",
247-
app_mode: 2,
248-
bitrate_level: song.level,
249-
_addrefer: `[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][23][cell_pc_songlist_song:2|page_pc_songlist_songflow|page_mine_like_music][${song.id}:song:x:x|:::|${source.id}:list::]`,
250-
_multirefers: [
251-
"[F:26][s][18][_ai]",
252-
"[F:26][s][12][_ai]",
253-
`[F:63][${now}#933#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][8][cell_pc_main_tab_entrance:6|page_pc_main_tab][我喜欢的音乐:spm::|:::]`,
254-
"[F:26][s][5][_ai]",
255-
"[F:26][s][0][_ai]",
256-
],
257-
vipType: ctx.auth.vipType,
258-
fee: 1,
259-
file: 4,
260-
rightSource: 0,
261-
sourceId: source.id,
262-
sourcetype: source.type,
263-
libra_abt: "",
264-
channel: ctx.app.channel,
265-
curStartChannel: "",
266-
};
267-
};
268-
269-
export const buildPld = (
270-
ctx: NcblContext,
271-
song: NcblSong,
272-
source: NcblSource,
273-
played: number,
274-
): Record<string, unknown> => {
275-
const now = Date.now();
212+
export const extractContext = (cookieObj: Record<string, string>): NcblContext => {
213+
const playbackLog = createPlaybackLogContext(cookieObj);
276214
return {
277-
mode: "circulation",
278-
download: 0,
279-
alg: "",
280-
status: "front",
281-
id: String(song.id),
282-
time: played,
283-
type: "song",
284-
is_listentogether: 0,
285-
source: source.name,
286-
is_heart: 0,
287-
realtime: played,
288-
resource_ratio: "",
289-
resource_time: song.time,
290-
musiceffect_id: "1001",
291-
app_mode: 1,
292-
lyriceffect: "default",
293-
displayMode: "classic",
294-
bitrate: song.bitrate,
295-
bitrate_level: song.level,
296-
_addrefer: `[F:63][${now}#616#${ctx.app.version}#${ctx.app.versionCode}#c9156c3][e][2][92][btn_pc_cover_play|cell_pc_songlist_song:6|page_pc_songlist_songflow|page_mine_like_music][:::|${song.id}:song:x:x|:::|${source.id}:list::]`,
297-
_multirefers: [
298-
"[F:26][s][87][_ai]",
299-
"[F:26][s][81][_ai]",
300-
"[F:26][s][75][_ai]",
301-
"[F:26][s][69][_ai]",
302-
"[F:26][s][63][_ai]",
303-
],
304-
vipType: ctx.auth.vipType,
305-
fee: 8,
306-
file: 4,
307-
rightSource: 0,
308-
sourceId: source.id,
309-
sourcetype: source.type,
310-
end: "interrupt",
311-
libra_abt: "",
312-
channel: ctx.app.channel,
313-
curStartChannel: "",
215+
app: {
216+
id: cookieObj.appid || "",
217+
urs: "",
218+
pid: "",
219+
nsm: cookieObj.WEVNSM || "1.0.0",
220+
cid: cookieObj.WNMCID || `${randomBytes(3).toString("hex")}.${Date.now()}.01.0`,
221+
channel: playbackLog.app.channel,
222+
version: playbackLog.app.version,
223+
versionCode: playbackLog.app.versionCode,
224+
buildCode: cookieObj.buildver || "",
225+
buildType: "release",
226+
packageId: "",
227+
},
228+
device: {
229+
id: cookieObj.deviceId || cookieObj.sDeviceId || "",
230+
ti: cookieObj.NMTID || "",
231+
sign: cookieObj.clientSign || "",
232+
model: cookieObj.mode || cookieObj.mobilename || "",
233+
nnid: cookieObj._ntes_nnid || ",",
234+
nuid: cookieObj._ntes_nuid || "",
235+
csrf: cookieObj.__csrf || "",
236+
systemType: cookieObj.os || "pc",
237+
systemVersion: cookieObj.osver || "Microsoft-Windows-10-Professional-build-19045-64bit",
238+
},
239+
auth: {
240+
token: cookieObj.MUSIC_U || "",
241+
sessionId: cookieObj["JSESSIONID-WYYY"] || "",
242+
vipType: playbackLog.auth.vipType,
243+
},
244+
startTime: Date.now(),
245+
processId: Math.floor(Math.random() * 90000) + 10000,
314246
};
315247
};
316248

317-
export const parseCookie = (cookie: unknown): Record<string, string> => {
318-
if (cookie && typeof cookie === "object") return cookie as Record<string, string>;
319-
if (typeof cookie !== "string") return {};
320-
const obj: Record<string, string> = {};
321-
for (const part of cookie.split(";")) {
322-
const idx = part.indexOf("=");
323-
if (idx <= 0) continue;
324-
const key = part.substring(0, idx).trim();
325-
const val = part.substring(idx + 1).trim();
326-
if (key) obj[key] = val;
327-
}
328-
return obj;
329-
};
330-
331-
export const extractContext = (cookieObj: Record<string, string>): NcblContext => ({
332-
app: {
333-
id: cookieObj.appid || "",
334-
urs: "",
335-
pid: "",
336-
nsm: cookieObj.WEVNSM || "1.0.0",
337-
cid: cookieObj.WNMCID || `${randomBytes(3).toString("hex")}.${Date.now()}.01.0`,
338-
channel: cookieObj.channel || "netease",
339-
version: cookieObj.appver || "3.1.35",
340-
versionCode: cookieObj.versioncode || "205293",
341-
buildCode: cookieObj.buildver || "",
342-
buildType: "release",
343-
packageId: "",
344-
},
345-
device: {
346-
id: cookieObj.deviceId || cookieObj.sDeviceId || "",
347-
ti: cookieObj.NMTID || "",
348-
sign: cookieObj.clientSign || "",
349-
model: cookieObj.mode || cookieObj.mobilename || "",
350-
nnid: cookieObj._ntes_nnid || ",",
351-
nuid: cookieObj._ntes_nuid || "",
352-
csrf: cookieObj.__csrf || "",
353-
systemType: cookieObj.os || "pc",
354-
systemVersion: cookieObj.osver || "Microsoft-Windows-10-Professional-build-19045-64bit",
355-
},
356-
auth: {
357-
token: cookieObj.MUSIC_U || "",
358-
sessionId: cookieObj["JSESSIONID-WYYY"] || "",
359-
vipType: cookieObj.vipType || "",
360-
},
361-
startTime: Date.now(),
362-
processId: Math.floor(Math.random() * 90000) + 10000,
363-
});
364-
365249
const randomHexId = (): string => cryptoRandomUUID().replace(/-/g, "");
366250

367251
export const buildMultipart = (
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import assert from "node:assert/strict";
2+
import { describe, it } from "node:test";
3+
import { buildPld, buildPlv, createPlaybackLogContext, toNcblSourceType } from "./playLog";
4+
5+
const context = createPlaybackLogContext({});
6+
const song = {
7+
id: 123,
8+
type: "song" as const,
9+
name: "测试歌曲",
10+
artist: "测试歌手",
11+
bitrate: 320,
12+
level: "exhigh",
13+
fee: 1,
14+
time: 180,
15+
};
16+
17+
describe("网易云 NCBL 播放日志", () => {
18+
const sourceCases = [
19+
["song", "song", "track"],
20+
["song", "list", "list"],
21+
["song", "album", "album"],
22+
["song", "artist", "artist"],
23+
["dj", "radio", "djradio"],
24+
] as const;
25+
26+
for (const [resourceType, sourceType, expected] of sourceCases) {
27+
it(`将 ${resourceType} 资源的 ${sourceType} 来源映射为 ${expected}`, () => {
28+
assert.equal(toNcblSourceType(resourceType, sourceType), expected);
29+
});
30+
}
31+
32+
it("歌单与专辑保留来源 ID、类型和真实 fee", () => {
33+
const plv = buildPlv(context, song, { id: "456", type: "list", name: "list" });
34+
const pld = buildPld(context, song, { id: "999", type: "album", name: "album" }, 60);
35+
assert.deepEqual(
36+
{
37+
id: plv.id,
38+
sourceId: plv.sourceId,
39+
sourcetype: plv.sourcetype,
40+
fee: plv.fee,
41+
},
42+
{ id: "123", sourceId: "456", sourcetype: "list", fee: 1 },
43+
);
44+
assert.deepEqual(
45+
{
46+
sourceId: pld.sourceId,
47+
sourcetype: pld.sourcetype,
48+
fee: pld.fee,
49+
},
50+
{ sourceId: "999", sourcetype: "album", fee: 1 },
51+
);
52+
assert.equal("_addrefer" in plv, false);
53+
assert.equal("_multirefers" in pld, false);
54+
});
55+
56+
it("声音以节目 ID 和 dj 类型写入日志", () => {
57+
const voice = {
58+
...song,
59+
id: 3081133072,
60+
type: "dj" as const,
61+
categoryId: 7,
62+
};
63+
const source = { id: "9988", type: "djradio", name: "djradio" };
64+
65+
for (const log of [buildPlv(context, voice, source), buildPld(context, voice, source, 60)]) {
66+
assert.deepEqual(
67+
{
68+
id: log.id,
69+
type: log.type,
70+
sourceId: log.sourceId,
71+
sourcetype: log.sourcetype,
72+
categoryId: log.categoryId,
73+
},
74+
{
75+
id: "3081133072",
76+
type: "dj",
77+
sourceId: "9988",
78+
sourcetype: "djradio",
79+
categoryId: 7,
80+
},
81+
);
82+
}
83+
});
84+
});

0 commit comments

Comments
 (0)