@bililive-tools/douyin-recorder 1.5.2 → 1.6.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 +4 -2
- package/lib/douyin_api.d.ts +6 -1
- package/lib/douyin_api.js +177 -46
- package/lib/index.js +25 -8
- package/lib/sign.d.ts +35 -0
- package/lib/sign.js +295 -0
- package/lib/stream.d.ts +5 -1
- package/lib/stream.js +3 -3
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +67 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ interface Options {
|
|
|
45
45
|
videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
|
|
46
46
|
useServerTimestamp?: boolean; // 控制弹幕是否使用服务端时间戳,默认为true
|
|
47
47
|
doubleScreen?: boolean; // 是否使用双屏直播流,开启后如果是双屏直播,那么就使用拼接的流,默认为true
|
|
48
|
-
|
|
48
|
+
recorderType?: "auto" | "ffmpeg" | "mesio"; // 底层录制器,使用mesio时videoFormat参数无效
|
|
49
|
+
auth?: string; // 传递cookie,
|
|
50
|
+
api?: "web" | "webHTML"; // 使用不同的接口
|
|
49
51
|
}
|
|
50
52
|
```
|
|
51
53
|
|
|
@@ -71,7 +73,7 @@ interface Options {
|
|
|
71
73
|
import { provider } from "@bililive-tools/douyin-recorder";
|
|
72
74
|
|
|
73
75
|
const url = "https://live.douyin.com/203641303310";
|
|
74
|
-
// 同样支持解析 https://v.douyin.com/DpfoBLAXoHM/
|
|
76
|
+
// 同样支持解析 https://v.douyin.com/DpfoBLAXoHM/, https://www.douyin.com/user/MS4wLjABAAAAE2ebAEBniL_0rF0vIDV4vCpdcH5RxpYBovopAURblNs
|
|
75
77
|
const { id } = await provider.resolveChannelInfoFromURL(url);
|
|
76
78
|
```
|
|
77
79
|
|
package/lib/douyin_api.d.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
export declare function resolveShortURL(shortURL: string): Promise<string>;
|
|
7
7
|
export declare const getCookie: () => Promise<string>;
|
|
8
8
|
export declare function getRoomInfo(webRoomId: string, opts?: {
|
|
9
|
-
retryOnSpecialCode?: boolean;
|
|
10
9
|
auth?: string;
|
|
11
10
|
doubleScreen?: boolean;
|
|
11
|
+
api?: "web" | "webHTML";
|
|
12
12
|
}): Promise<{
|
|
13
13
|
living: boolean;
|
|
14
14
|
roomId: string;
|
|
@@ -20,6 +20,11 @@ export declare function getRoomInfo(webRoomId: string, opts?: {
|
|
|
20
20
|
cover: string;
|
|
21
21
|
liveId: string;
|
|
22
22
|
}>;
|
|
23
|
+
/**
|
|
24
|
+
* 解析抖音号
|
|
25
|
+
* @param url
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseUser(url: string): Promise<any>;
|
|
23
28
|
export interface StreamProfile {
|
|
24
29
|
desc: string;
|
|
25
30
|
key: string;
|
package/lib/douyin_api.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { URL, URLSearchParams } from "url";
|
|
1
2
|
import axios from "axios";
|
|
2
3
|
import { isEmpty } from "lodash-es";
|
|
3
|
-
import { assert } from "./utils.js";
|
|
4
|
+
import { assert, get__ac_signature } from "./utils.js";
|
|
5
|
+
import { ABogus } from "./sign.js";
|
|
4
6
|
const requester = axios.create({
|
|
5
7
|
timeout: 10e3,
|
|
6
8
|
// axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,这会让请求发往代理的 host。
|
|
@@ -18,6 +20,14 @@ const requester = axios.create({
|
|
|
18
20
|
export async function resolveShortURL(shortURL) {
|
|
19
21
|
// 获取跳转后的页面内容
|
|
20
22
|
const response = await requester.get(shortURL);
|
|
23
|
+
const redirectedURL = response.request.res.responseUrl;
|
|
24
|
+
if (redirectedURL.includes("/user/")) {
|
|
25
|
+
const secUid = new URL(redirectedURL).searchParams.get("sec_uid");
|
|
26
|
+
if (!secUid) {
|
|
27
|
+
throw new Error("无法从短链接解析出直播间ID");
|
|
28
|
+
}
|
|
29
|
+
return parseUser(`https://www.douyin.com/user/${secUid}`);
|
|
30
|
+
}
|
|
21
31
|
// 尝试从页面内容中提取webRid
|
|
22
32
|
const webRidMatch = response.data.match(/"webRid\\":\\"(\d+)\\"/);
|
|
23
33
|
if (webRidMatch) {
|
|
@@ -84,7 +94,77 @@ export const getCookie = async () => {
|
|
|
84
94
|
};
|
|
85
95
|
return cookies;
|
|
86
96
|
};
|
|
87
|
-
|
|
97
|
+
function generateNonce() {
|
|
98
|
+
// 21味随机字母数字组合
|
|
99
|
+
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
100
|
+
let nonce = "";
|
|
101
|
+
for (let i = 0; i < 21; i++) {
|
|
102
|
+
nonce += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
103
|
+
}
|
|
104
|
+
return nonce;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 通过解析html页面来获取房间数据
|
|
108
|
+
* @param webRoomId
|
|
109
|
+
* @param opts
|
|
110
|
+
*/
|
|
111
|
+
async function getRoomInfoByHtml(webRoomId, opts = {}) {
|
|
112
|
+
const url = `https://live.douyin.com/${webRoomId}`;
|
|
113
|
+
const ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0";
|
|
114
|
+
const nonce = generateNonce();
|
|
115
|
+
let cookies = undefined;
|
|
116
|
+
if (opts.auth) {
|
|
117
|
+
cookies = opts.auth;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
121
|
+
const signed = get__ac_signature(timestamp, url, nonce, ua);
|
|
122
|
+
cookies = `__ac_nonce=${nonce}; __ac_signature=${signed}; __ac_referer=__ac_blank`;
|
|
123
|
+
}
|
|
124
|
+
const res = await axios.get(url, {
|
|
125
|
+
headers: {
|
|
126
|
+
"User-Agent": ua,
|
|
127
|
+
cookie: cookies,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const regex = /(\{\\"state\\":.*?)\]\\n"\]\)/;
|
|
131
|
+
const match = res.data.match(regex);
|
|
132
|
+
if (!match) {
|
|
133
|
+
throw new Error("No match found in HTML");
|
|
134
|
+
}
|
|
135
|
+
let jsonStr = match[1];
|
|
136
|
+
jsonStr = jsonStr.replace(/\\"/g, '"');
|
|
137
|
+
jsonStr = jsonStr.replace(/\\"/g, '"');
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(jsonStr);
|
|
140
|
+
const roomInfo = data.state.roomStore.roomInfo;
|
|
141
|
+
const streamData = data.state.streamStore.streamData;
|
|
142
|
+
return {
|
|
143
|
+
living: roomInfo.room.status === 2,
|
|
144
|
+
nickname: roomInfo.anchor.nickname,
|
|
145
|
+
avatar: roomInfo.anchor?.avatar_thumb?.url_list?.[0],
|
|
146
|
+
room: {
|
|
147
|
+
title: roomInfo.room.title,
|
|
148
|
+
cover: roomInfo.room.cover?.url_list?.[0],
|
|
149
|
+
id_str: roomInfo.room.id_str,
|
|
150
|
+
stream_url: {
|
|
151
|
+
pull_datas: roomInfo.room?.stream_url?.pull_datas,
|
|
152
|
+
live_core_sdk_data: {
|
|
153
|
+
pull_data: {
|
|
154
|
+
options: { qualities: streamData.H264_streamData?.options?.qualities ?? [] },
|
|
155
|
+
stream_data: streamData.H264_streamData?.stream ?? {},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
console.error("Failed to parse JSON:", e);
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function getRoomInfoByWeb(webRoomId, opts = {}) {
|
|
88
168
|
let cookies = undefined;
|
|
89
169
|
if (opts.auth) {
|
|
90
170
|
cookies = opts.auth;
|
|
@@ -94,59 +174,67 @@ export async function getRoomInfo(webRoomId, opts = {}) {
|
|
|
94
174
|
// 所以在这里请求一次自动设置。
|
|
95
175
|
cookies = await getCookie();
|
|
96
176
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
177
|
+
const params = {
|
|
178
|
+
aid: 6383,
|
|
179
|
+
live_id: 1,
|
|
180
|
+
device_platform: "web",
|
|
181
|
+
language: "zh-CN",
|
|
182
|
+
enter_from: "web_live",
|
|
183
|
+
cookie_enabled: "true",
|
|
184
|
+
screen_width: 1920,
|
|
185
|
+
screen_height: 1080,
|
|
186
|
+
browser_language: "zh-CN",
|
|
187
|
+
browser_platform: "MacIntel",
|
|
188
|
+
browser_name: "Chrome",
|
|
189
|
+
browser_version: "108.0.0.0",
|
|
190
|
+
web_rid: webRoomId,
|
|
191
|
+
"Room-Enter-User-Login-Ab": 0,
|
|
192
|
+
is_need_double_stream: "false",
|
|
193
|
+
};
|
|
194
|
+
const abogus = new ABogus();
|
|
195
|
+
const [query, _, ua] = abogus.generateAbogus(new URLSearchParams(params).toString(), "");
|
|
196
|
+
const res = await requester.get(`https://live.douyin.com/webcast/room/web/enter/?${query}`, {
|
|
117
197
|
headers: {
|
|
118
198
|
cookie: cookies,
|
|
199
|
+
"User-Agent": ua,
|
|
119
200
|
},
|
|
120
201
|
});
|
|
121
|
-
// 无 cookie 时 code 为 10037
|
|
122
|
-
if (res.data.status_code === 10037 && opts.retryOnSpecialCode) {
|
|
123
|
-
// resp 自动设置 cookie
|
|
124
|
-
// const cookieRes = await requester.get("https://live.douyin.com/favicon.ico");
|
|
125
|
-
// const cookies = cookieRes.headers["set-cookie"]
|
|
126
|
-
// .map((cookie) => {
|
|
127
|
-
// return cookie.split(";")[0];
|
|
128
|
-
// })
|
|
129
|
-
// .join("; ");
|
|
130
|
-
// console.log("cookies", cookies);
|
|
131
|
-
return getRoomInfo(webRoomId, {
|
|
132
|
-
retryOnSpecialCode: false,
|
|
133
|
-
doubleScreen: opts.doubleScreen,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
202
|
assert(res.data.status_code === 0, `Unexpected resp, code ${res.data.status_code}, msg ${JSON.stringify(res.data.data)}, id ${webRoomId}, cookies: ${cookies}`);
|
|
137
203
|
const data = res.data.data;
|
|
138
204
|
const room = data.data[0];
|
|
139
205
|
assert(room, `No room data, id ${webRoomId}`);
|
|
206
|
+
return {
|
|
207
|
+
living: data.room_status === 0,
|
|
208
|
+
nickname: data.user.nickname,
|
|
209
|
+
avatar: data?.user?.avatar_thumb?.url_list?.[0],
|
|
210
|
+
room: {
|
|
211
|
+
title: room.title,
|
|
212
|
+
cover: room.cover?.url_list?.[0],
|
|
213
|
+
id_str: room.id_str,
|
|
214
|
+
stream_url: room.stream_url,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export async function getRoomInfo(webRoomId, opts = {}) {
|
|
219
|
+
let data;
|
|
220
|
+
if (opts.api === "webHTML") {
|
|
221
|
+
data = await getRoomInfoByHtml(webRoomId, opts);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
data = await getRoomInfoByWeb(webRoomId, opts);
|
|
225
|
+
}
|
|
226
|
+
const room = data.room;
|
|
227
|
+
assert(room, `No room data, id ${webRoomId}`);
|
|
140
228
|
if (room?.stream_url == null) {
|
|
141
229
|
return {
|
|
142
230
|
living: false,
|
|
143
231
|
roomId: webRoomId,
|
|
144
|
-
owner: data.
|
|
145
|
-
title: room?.title ?? data.
|
|
232
|
+
owner: data.nickname,
|
|
233
|
+
title: room?.title ?? data.nickname,
|
|
146
234
|
streams: [],
|
|
147
235
|
sources: [],
|
|
148
|
-
avatar: data.
|
|
149
|
-
cover: room.cover
|
|
236
|
+
avatar: data.avatar,
|
|
237
|
+
cover: room.cover,
|
|
150
238
|
liveId: room.id_str,
|
|
151
239
|
};
|
|
152
240
|
}
|
|
@@ -159,14 +247,16 @@ export async function getRoomInfo(webRoomId, opts = {}) {
|
|
|
159
247
|
},
|
|
160
248
|
stream_data: "",
|
|
161
249
|
};
|
|
250
|
+
// @ts-ignore
|
|
162
251
|
qualities = pull_data.options.qualities;
|
|
252
|
+
// @ts-ignore
|
|
163
253
|
stream_data = pull_data.stream_data;
|
|
164
254
|
}
|
|
165
255
|
if (!stream_data) {
|
|
166
256
|
qualities = room.stream_url.live_core_sdk_data.pull_data.options.qualities;
|
|
167
257
|
stream_data = room.stream_url.live_core_sdk_data.pull_data.stream_data;
|
|
168
258
|
}
|
|
169
|
-
const streamData = JSON.parse(stream_data).data;
|
|
259
|
+
const streamData = typeof stream_data === "string" ? JSON.parse(stream_data).data : stream_data;
|
|
170
260
|
const streams = qualities.map((info) => ({
|
|
171
261
|
desc: info.name,
|
|
172
262
|
key: info.sdk_key,
|
|
@@ -215,14 +305,55 @@ export async function getRoomInfo(webRoomId, opts = {}) {
|
|
|
215
305
|
];
|
|
216
306
|
// console.log(JSON.stringify(sources, null, 2), qualities);
|
|
217
307
|
return {
|
|
218
|
-
living: data.
|
|
308
|
+
living: data.living,
|
|
219
309
|
roomId: webRoomId,
|
|
220
|
-
owner: data.
|
|
310
|
+
owner: data.nickname,
|
|
221
311
|
title: room.title,
|
|
222
312
|
streams,
|
|
223
313
|
sources,
|
|
224
|
-
avatar: data.
|
|
225
|
-
cover: room.cover
|
|
314
|
+
avatar: data.avatar,
|
|
315
|
+
cover: room.cover,
|
|
226
316
|
liveId: room.id_str,
|
|
227
317
|
};
|
|
228
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* 获取nonce
|
|
321
|
+
*/
|
|
322
|
+
async function getNonce(url) {
|
|
323
|
+
const res = await requester.get(url);
|
|
324
|
+
if (!res.headers["set-cookie"]) {
|
|
325
|
+
throw new Error("No cookie in response");
|
|
326
|
+
}
|
|
327
|
+
const cookies = {};
|
|
328
|
+
(res.headers["set-cookie"] ?? []).forEach((cookie) => {
|
|
329
|
+
const [key, _] = cookie.split(";");
|
|
330
|
+
const [keyPart, valuePart] = key.split("=");
|
|
331
|
+
if (!keyPart || !valuePart)
|
|
332
|
+
return;
|
|
333
|
+
cookies[keyPart.trim()] = valuePart.trim();
|
|
334
|
+
});
|
|
335
|
+
return cookies["__ac_nonce"];
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 解析抖音号
|
|
339
|
+
* @param url
|
|
340
|
+
*/
|
|
341
|
+
export async function parseUser(url) {
|
|
342
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
343
|
+
const ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0";
|
|
344
|
+
const nonce = (await getNonce(url)) ?? generateNonce();
|
|
345
|
+
const signed = get__ac_signature(timestamp, url, nonce, ua);
|
|
346
|
+
const res = await requester.get(url, {
|
|
347
|
+
headers: {
|
|
348
|
+
"User-Agent": ua,
|
|
349
|
+
cookie: `__ac_nonce=${nonce}; __ac_signature=${signed}`,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
const text = res.data;
|
|
353
|
+
const regex = /\\"uniqueId\\":\\"(.*?)\\"/;
|
|
354
|
+
const match = text.match(regex);
|
|
355
|
+
if (match && match[1]) {
|
|
356
|
+
return match[1];
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import mitt from "mitt";
|
|
3
|
-
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID,
|
|
3
|
+
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, createBaseRecorder, } from "@bililive-tools/manager";
|
|
4
4
|
import { getInfo, getStream } from "./stream.js";
|
|
5
5
|
import { ensureFolderExist, singleton } from "./utils.js";
|
|
6
|
-
import { resolveShortURL } from "./douyin_api.js";
|
|
6
|
+
import { resolveShortURL, parseUser } from "./douyin_api.js";
|
|
7
7
|
import DouYinDanmaClient from "douyin-danma-listener";
|
|
8
8
|
function createRecorder(opts) {
|
|
9
9
|
// 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过
|
|
@@ -29,7 +29,10 @@ function createRecorder(opts) {
|
|
|
29
29
|
},
|
|
30
30
|
async getLiveInfo() {
|
|
31
31
|
const channelId = this.channelId;
|
|
32
|
-
const info = await getInfo(channelId
|
|
32
|
+
const info = await getInfo(channelId, {
|
|
33
|
+
cookie: this.auth,
|
|
34
|
+
api: this.api,
|
|
35
|
+
});
|
|
33
36
|
return {
|
|
34
37
|
channelId,
|
|
35
38
|
...info,
|
|
@@ -77,7 +80,10 @@ const ffmpegInputOptions = [
|
|
|
77
80
|
const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
|
|
78
81
|
if (this.recordHandle != null)
|
|
79
82
|
return this.recordHandle;
|
|
80
|
-
const liveInfo = await getInfo(this.channelId
|
|
83
|
+
const liveInfo = await getInfo(this.channelId, {
|
|
84
|
+
cookie: this.auth,
|
|
85
|
+
api: this.api,
|
|
86
|
+
});
|
|
81
87
|
const { living, owner, title } = liveInfo;
|
|
82
88
|
this.liveInfo = liveInfo;
|
|
83
89
|
if (liveInfo.liveId === banLiveId) {
|
|
@@ -111,9 +117,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
111
117
|
cookie: this.auth,
|
|
112
118
|
formatPriorities: this.formatPriorities,
|
|
113
119
|
doubleScreen: this.doubleScreen,
|
|
120
|
+
api: this.api,
|
|
114
121
|
});
|
|
115
122
|
}
|
|
116
123
|
catch (err) {
|
|
124
|
+
if (this.qualityRetry > 0)
|
|
125
|
+
this.qualityRetry -= 1;
|
|
117
126
|
this.state = "idle";
|
|
118
127
|
throw err;
|
|
119
128
|
}
|
|
@@ -123,7 +132,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
123
132
|
this.availableSources = availableSources.map((s) => s.name);
|
|
124
133
|
this.usedStream = stream.name;
|
|
125
134
|
this.usedSource = stream.source;
|
|
126
|
-
// TODO: emit update event
|
|
127
135
|
let isEnded = false;
|
|
128
136
|
let isCutting = false;
|
|
129
137
|
const onEnd = (...args) => {
|
|
@@ -136,12 +144,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
136
144
|
isEnded = true;
|
|
137
145
|
this.emit("DebugLog", {
|
|
138
146
|
type: "common",
|
|
139
|
-
text: `
|
|
147
|
+
text: `record end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
|
|
140
148
|
});
|
|
141
149
|
const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
|
|
142
150
|
this.recordHandle?.stop(reason);
|
|
143
151
|
};
|
|
144
|
-
|
|
152
|
+
let recorderType = this.recorderType === "mesio" ? "mesio" : "ffmpeg";
|
|
153
|
+
// TODO:测试只录制音频,hls以及fmp4
|
|
154
|
+
const recorder = createBaseRecorder(recorderType, {
|
|
145
155
|
url: stream.url,
|
|
146
156
|
outputOptions: ffmpegOutputOptions,
|
|
147
157
|
inputOptions: ffmpegInputOptions,
|
|
@@ -235,7 +245,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
|
|
|
235
245
|
type: "give_gift",
|
|
236
246
|
timestamp: this.useServerTimestamp ? serverTimestamp : Date.now(),
|
|
237
247
|
name: msg.gift.name,
|
|
238
|
-
price:
|
|
248
|
+
price: msg.gift.diamondCount / 10 || 0,
|
|
239
249
|
count: Number(msg.totalCount ?? 1),
|
|
240
250
|
color: "#ffffff",
|
|
241
251
|
sender: {
|
|
@@ -362,6 +372,13 @@ export const provider = {
|
|
|
362
372
|
throw new Error(`解析抖音短链接失败: ${err?.message}`);
|
|
363
373
|
}
|
|
364
374
|
}
|
|
375
|
+
else if (channelURL.includes("/user/")) {
|
|
376
|
+
// 解析用户主页
|
|
377
|
+
id = await parseUser(channelURL);
|
|
378
|
+
if (!id) {
|
|
379
|
+
throw new Error(`解析抖音用户主页失败`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
365
382
|
else {
|
|
366
383
|
// 处理常规直播链接
|
|
367
384
|
id = path.basename(new URL(channelURL).pathname);
|
package/lib/sign.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare class StringProcessor {
|
|
2
|
+
static toCharStr(bytes: Uint8Array): string;
|
|
3
|
+
static toCharArray(s: string): number[];
|
|
4
|
+
static generateRandomBytes(length: number): string;
|
|
5
|
+
}
|
|
6
|
+
export declare class CryptoUtility {
|
|
7
|
+
salt: string;
|
|
8
|
+
base64Alphabet: string[][];
|
|
9
|
+
bigArray: number[];
|
|
10
|
+
constructor(salt: string, customBase64Alphabet: string[]);
|
|
11
|
+
static sm3ToArray(input: Uint8Array | string): number[];
|
|
12
|
+
addSalt(param: string): string;
|
|
13
|
+
paramsToArray(param: string, addSalt: boolean): number[];
|
|
14
|
+
transformBytes(valuesList: number[]): number[];
|
|
15
|
+
base64Encode(bytes: Uint8Array, selectedAlphabet: number): string;
|
|
16
|
+
abogusEncode(values: number[], selectedAlphabet: number): string;
|
|
17
|
+
static rc4Encrypt(key: number[], plaintext: string): Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
export declare class BrowserFingerprintGenerator {
|
|
20
|
+
static generateFingerprint(browserType: string): string;
|
|
21
|
+
private static _generateFingerprint;
|
|
22
|
+
}
|
|
23
|
+
export declare class ABogus {
|
|
24
|
+
cryptoUtility: CryptoUtility;
|
|
25
|
+
userAgent: string;
|
|
26
|
+
browserFp: string;
|
|
27
|
+
options: number[];
|
|
28
|
+
pageId: number;
|
|
29
|
+
aid: number;
|
|
30
|
+
uaKey: number[];
|
|
31
|
+
sortIndex: number[];
|
|
32
|
+
sortIndex2: number[];
|
|
33
|
+
constructor(fp?: string, userAgent?: string, options?: number[]);
|
|
34
|
+
generateAbogus(params: string, body: string): [string, string, string, string];
|
|
35
|
+
}
|
package/lib/sign.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// abogus.ts
|
|
2
|
+
// 这是对 Rust 版 ABogus 的 TypeScript 实现
|
|
3
|
+
// 代码来自:https://github.com/hua0512/rust-srec/blob/6444641014ea58628af9b0fa51b099620a01d0d0/crates/platforms/src/extractor/platforms/douyin/abogus.rs
|
|
4
|
+
// 依赖 sm3、rc4、random 等库
|
|
5
|
+
import { sm3 } from "sm-crypto"; // npm install sm-crypto
|
|
6
|
+
export class StringProcessor {
|
|
7
|
+
static toCharStr(bytes) {
|
|
8
|
+
return Array.from(bytes)
|
|
9
|
+
.map((b) => String.fromCharCode(b))
|
|
10
|
+
.join("");
|
|
11
|
+
}
|
|
12
|
+
static toCharArray(s) {
|
|
13
|
+
return Array.from(s).map((c) => c.charCodeAt(0));
|
|
14
|
+
}
|
|
15
|
+
static generateRandomBytes(length) {
|
|
16
|
+
const result = [];
|
|
17
|
+
for (let i = 0; i < length; i++) {
|
|
18
|
+
const rd = Math.floor(Math.random() * 10000);
|
|
19
|
+
result.push((rd & 255 & 170) | 1);
|
|
20
|
+
result.push((rd & 255 & 85) | 2);
|
|
21
|
+
result.push(((rd >> 8) & 170) | 5);
|
|
22
|
+
result.push(((rd >> 8) & 85) | 40);
|
|
23
|
+
}
|
|
24
|
+
return this.toCharStr(Uint8Array.from(result));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class CryptoUtility {
|
|
28
|
+
salt;
|
|
29
|
+
base64Alphabet;
|
|
30
|
+
bigArray;
|
|
31
|
+
constructor(salt, customBase64Alphabet) {
|
|
32
|
+
this.salt = salt;
|
|
33
|
+
this.base64Alphabet = customBase64Alphabet.map((s) => Array.from(s));
|
|
34
|
+
this.bigArray = [
|
|
35
|
+
121, 243, 55, 234, 103, 36, 47, 228, 30, 231, 106, 6, 115, 95, 78, 101, 250, 207, 198, 50,
|
|
36
|
+
139, 227, 220, 105, 97, 143, 34, 28, 194, 215, 18, 100, 159, 160, 43, 8, 169, 217, 180, 120,
|
|
37
|
+
247, 45, 90, 11, 27, 197, 46, 3, 84, 72, 5, 68, 62, 56, 221, 75, 144, 79, 73, 161, 178, 81,
|
|
38
|
+
64, 187, 134, 117, 186, 118, 16, 241, 130, 71, 89, 147, 122, 129, 65, 40, 88, 150, 110, 219,
|
|
39
|
+
199, 255, 181, 254, 48, 4, 195, 248, 208, 32, 116, 167, 69, 201, 17, 124, 125, 104, 96, 83,
|
|
40
|
+
80, 127, 236, 108, 154, 126, 204, 15, 20, 135, 112, 158, 13, 1, 188, 164, 210, 237, 222, 98,
|
|
41
|
+
212, 77, 253, 42, 170, 202, 26, 22, 29, 182, 251, 10, 173, 152, 58, 138, 54, 141, 185, 33,
|
|
42
|
+
157, 31, 252, 132, 233, 235, 102, 196, 191, 223, 240, 148, 39, 123, 92, 82, 128, 109, 57, 24,
|
|
43
|
+
38, 113, 209, 245, 2, 119, 153, 229, 189, 214, 230, 174, 232, 63, 52, 205, 86, 140, 66, 175,
|
|
44
|
+
111, 171, 246, 133, 238, 193, 99, 60, 74, 91, 225, 51, 76, 37, 145, 211, 166, 151, 213, 206,
|
|
45
|
+
0, 200, 244, 176, 218, 44, 184, 172, 49, 216, 93, 168, 53, 21, 183, 41, 67, 85, 224, 155, 226,
|
|
46
|
+
242, 87, 177, 146, 70, 190, 12, 162, 19, 137, 114, 25, 165, 163, 192, 23, 59, 9, 94, 179, 107,
|
|
47
|
+
35, 7, 142, 131, 239, 203, 149, 136, 61, 249, 14, 156,
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
static sm3ToArray(input) {
|
|
51
|
+
const hash = sm3(input instanceof Uint8Array ? Buffer.from(input) : input);
|
|
52
|
+
return Buffer.from(hash, "hex").toJSON().data;
|
|
53
|
+
}
|
|
54
|
+
addSalt(param) {
|
|
55
|
+
return param + this.salt;
|
|
56
|
+
}
|
|
57
|
+
paramsToArray(param, addSalt) {
|
|
58
|
+
const processed = addSalt ? this.addSalt(param) : param;
|
|
59
|
+
return CryptoUtility.sm3ToArray(processed);
|
|
60
|
+
}
|
|
61
|
+
transformBytes(valuesList) {
|
|
62
|
+
const result = [];
|
|
63
|
+
let indexB = this.bigArray[1];
|
|
64
|
+
let initialValue = 0, valueE = 0;
|
|
65
|
+
const arrayLen = this.bigArray.length;
|
|
66
|
+
for (let index = 0; index < valuesList.length; index++) {
|
|
67
|
+
let sumInitial;
|
|
68
|
+
if (index === 0) {
|
|
69
|
+
initialValue = this.bigArray[indexB];
|
|
70
|
+
sumInitial = indexB + initialValue;
|
|
71
|
+
this.bigArray[1] = initialValue;
|
|
72
|
+
this.bigArray[indexB] = indexB;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
sumInitial = initialValue + valueE;
|
|
76
|
+
}
|
|
77
|
+
const sumInitialIdx = sumInitial % arrayLen;
|
|
78
|
+
const valueF = this.bigArray[sumInitialIdx];
|
|
79
|
+
result.push(valuesList[index] ^ valueF);
|
|
80
|
+
const nextIdx = (index + 2) % arrayLen;
|
|
81
|
+
valueE = this.bigArray[nextIdx];
|
|
82
|
+
const newSumInitialIdx = (indexB + valueE) % arrayLen;
|
|
83
|
+
initialValue = this.bigArray[newSumInitialIdx];
|
|
84
|
+
[this.bigArray[newSumInitialIdx], this.bigArray[nextIdx]] = [
|
|
85
|
+
this.bigArray[nextIdx],
|
|
86
|
+
this.bigArray[newSumInitialIdx],
|
|
87
|
+
];
|
|
88
|
+
indexB = newSumInitialIdx;
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
base64Encode(bytes, selectedAlphabet) {
|
|
93
|
+
const alphabet = this.base64Alphabet[selectedAlphabet];
|
|
94
|
+
let output = "";
|
|
95
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
96
|
+
const b1 = bytes[i];
|
|
97
|
+
const b2 = bytes[i + 1] || 0;
|
|
98
|
+
const b3 = bytes[i + 2] || 0;
|
|
99
|
+
const combined = (b1 << 16) | (b2 << 8) | b3;
|
|
100
|
+
output += alphabet[(combined >> 18) & 63];
|
|
101
|
+
output += alphabet[(combined >> 12) & 63];
|
|
102
|
+
output += i + 1 < bytes.length ? alphabet[(combined >> 6) & 63] : "";
|
|
103
|
+
output += i + 2 < bytes.length ? alphabet[combined & 63] : "";
|
|
104
|
+
}
|
|
105
|
+
while (output.length % 4 !== 0)
|
|
106
|
+
output += "=";
|
|
107
|
+
return output;
|
|
108
|
+
}
|
|
109
|
+
abogusEncode(values, selectedAlphabet) {
|
|
110
|
+
const alphabet = this.base64Alphabet[selectedAlphabet];
|
|
111
|
+
let abogus = "";
|
|
112
|
+
for (let i = 0; i < values.length; i += 3) {
|
|
113
|
+
const v1 = values[i];
|
|
114
|
+
const v2 = values[i + 1] || 0;
|
|
115
|
+
const v3 = values[i + 2] || 0;
|
|
116
|
+
const n = (v1 << 16) | (v2 << 8) | v3;
|
|
117
|
+
abogus += alphabet[(n & 0xfc0000) >> 18];
|
|
118
|
+
abogus += alphabet[(n & 0x03f000) >> 12];
|
|
119
|
+
abogus += i + 1 < values.length ? alphabet[(n & 0x0fc0) >> 6] : "";
|
|
120
|
+
abogus += i + 2 < values.length ? alphabet[n & 0x3f] : "";
|
|
121
|
+
}
|
|
122
|
+
while (abogus.length % 4 !== 0)
|
|
123
|
+
abogus += "=";
|
|
124
|
+
return abogus;
|
|
125
|
+
}
|
|
126
|
+
static rc4Encrypt(key, plaintext) {
|
|
127
|
+
// 推荐用 npm 包 rc4 或自己实现
|
|
128
|
+
const S = Array.from({ length: 256 }, (_, i) => i);
|
|
129
|
+
let j = 0;
|
|
130
|
+
for (let i = 0; i < 256; i++) {
|
|
131
|
+
j = (j + S[i] + key[i % key.length]) & 0xff;
|
|
132
|
+
[S[i], S[j]] = [S[j], S[i]];
|
|
133
|
+
}
|
|
134
|
+
let i = 0;
|
|
135
|
+
j = 0;
|
|
136
|
+
const ptBytes = StringProcessor.toCharArray(plaintext);
|
|
137
|
+
const ct = [];
|
|
138
|
+
for (const charVal of ptBytes) {
|
|
139
|
+
i = (i + 1) & 0xff;
|
|
140
|
+
j = (j + S[i]) & 0xff;
|
|
141
|
+
[S[i], S[j]] = [S[j], S[i]];
|
|
142
|
+
const k = S[(S[i] + S[j]) & 0xff];
|
|
143
|
+
ct.push(charVal ^ k);
|
|
144
|
+
}
|
|
145
|
+
return Uint8Array.from(ct);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export class BrowserFingerprintGenerator {
|
|
149
|
+
static generateFingerprint(browserType) {
|
|
150
|
+
switch (browserType) {
|
|
151
|
+
case "Chrome":
|
|
152
|
+
case "Edge":
|
|
153
|
+
case "Firefox":
|
|
154
|
+
return this._generateFingerprint("Win32");
|
|
155
|
+
case "Safari":
|
|
156
|
+
return this._generateFingerprint("MacIntel");
|
|
157
|
+
default:
|
|
158
|
+
return this._generateFingerprint("Win32");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
static _generateFingerprint(platform) {
|
|
162
|
+
const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
|
163
|
+
const innerWidth = rand(1024, 1920);
|
|
164
|
+
const innerHeight = rand(768, 1080);
|
|
165
|
+
const outerWidth = innerWidth + rand(24, 32);
|
|
166
|
+
const outerHeight = innerHeight + rand(75, 90);
|
|
167
|
+
const screenX = 0;
|
|
168
|
+
const screenY = [0, 30][rand(0, 1)];
|
|
169
|
+
const sizeWidth = rand(1024, 1920);
|
|
170
|
+
const sizeHeight = rand(768, 1080);
|
|
171
|
+
const availWidth = rand(1280, 1920);
|
|
172
|
+
const availHeight = rand(800, 1080);
|
|
173
|
+
return `${innerWidth}|${innerHeight}|${outerWidth}|${outerHeight}|${screenX}|${screenY}|0|0|${sizeWidth}|${sizeHeight}|${availWidth}|${availHeight}|${innerWidth}|${innerHeight}|24|24|${platform}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export class ABogus {
|
|
177
|
+
cryptoUtility;
|
|
178
|
+
userAgent;
|
|
179
|
+
browserFp;
|
|
180
|
+
options;
|
|
181
|
+
pageId;
|
|
182
|
+
aid;
|
|
183
|
+
uaKey;
|
|
184
|
+
sortIndex;
|
|
185
|
+
sortIndex2;
|
|
186
|
+
constructor(fp, userAgent, options) {
|
|
187
|
+
const salt = "cus";
|
|
188
|
+
const character = "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe";
|
|
189
|
+
const character2 = "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe";
|
|
190
|
+
const characterList = [character, character2];
|
|
191
|
+
this.cryptoUtility = new CryptoUtility(salt, characterList);
|
|
192
|
+
this.userAgent =
|
|
193
|
+
userAgent && userAgent.length > 0
|
|
194
|
+
? userAgent
|
|
195
|
+
: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0";
|
|
196
|
+
this.browserFp =
|
|
197
|
+
fp && fp.length > 0 ? fp : BrowserFingerprintGenerator.generateFingerprint("Edge");
|
|
198
|
+
this.options = options || [0, 1, 14];
|
|
199
|
+
this.pageId = 0;
|
|
200
|
+
this.aid = 6383;
|
|
201
|
+
this.uaKey = [0x00, 0x01, 0x0e];
|
|
202
|
+
this.sortIndex = [
|
|
203
|
+
18, 20, 52, 26, 30, 34, 58, 38, 40, 53, 42, 21, 27, 54, 55, 31, 35, 57, 39, 41, 43, 22, 28,
|
|
204
|
+
32, 60, 36, 23, 29, 33, 37, 44, 45, 59, 46, 47, 48, 49, 50, 24, 25, 65, 66, 70, 71,
|
|
205
|
+
];
|
|
206
|
+
this.sortIndex2 = [
|
|
207
|
+
18, 20, 26, 30, 34, 38, 40, 42, 21, 27, 31, 35, 39, 41, 43, 22, 28, 32, 36, 23, 29, 33, 37,
|
|
208
|
+
44, 45, 46, 47, 48, 49, 50, 24, 25, 52, 53, 54, 55, 57, 58, 59, 60, 65, 66, 70, 71,
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
generateAbogus(params, body) {
|
|
212
|
+
const abDir = {
|
|
213
|
+
8: 3,
|
|
214
|
+
18: 44,
|
|
215
|
+
66: 0,
|
|
216
|
+
69: 0,
|
|
217
|
+
70: 0,
|
|
218
|
+
71: 0,
|
|
219
|
+
};
|
|
220
|
+
const startEncryption = Date.now();
|
|
221
|
+
// Hash(Hash(params))
|
|
222
|
+
const paramsHash1 = this.cryptoUtility.paramsToArray(params, true);
|
|
223
|
+
// @ts-ignore
|
|
224
|
+
const array1 = CryptoUtility.sm3ToArray(paramsHash1);
|
|
225
|
+
// Hash(Hash(body))
|
|
226
|
+
const bodyHash1 = this.cryptoUtility.paramsToArray(body, true);
|
|
227
|
+
// @ts-ignore
|
|
228
|
+
const array2 = CryptoUtility.sm3ToArray(bodyHash1);
|
|
229
|
+
// Hash(Base64(RC4(user_agent)))
|
|
230
|
+
const rc4Ua = CryptoUtility.rc4Encrypt(this.uaKey, this.userAgent);
|
|
231
|
+
const uaB64 = this.cryptoUtility.base64Encode(rc4Ua, 1);
|
|
232
|
+
const array3 = this.cryptoUtility.paramsToArray(uaB64, false);
|
|
233
|
+
const endEncryption = Date.now();
|
|
234
|
+
// 动态填充 abDir
|
|
235
|
+
abDir[20] = (startEncryption >> 24) & 255;
|
|
236
|
+
abDir[21] = (startEncryption >> 16) & 255;
|
|
237
|
+
abDir[22] = (startEncryption >> 8) & 255;
|
|
238
|
+
abDir[23] = startEncryption & 255;
|
|
239
|
+
abDir[24] = Math.floor(startEncryption / 0x100000000);
|
|
240
|
+
abDir[25] = Math.floor(startEncryption / 0x10000000000);
|
|
241
|
+
abDir[26] = (this.options[0] >> 24) & 255;
|
|
242
|
+
abDir[27] = (this.options[0] >> 16) & 255;
|
|
243
|
+
abDir[28] = (this.options[0] >> 8) & 255;
|
|
244
|
+
abDir[29] = this.options[0] & 255;
|
|
245
|
+
abDir[30] = Math.floor(this.options[1] / 256) & 255;
|
|
246
|
+
abDir[31] = this.options[1] % 256;
|
|
247
|
+
abDir[32] = (this.options[1] >> 24) & 255;
|
|
248
|
+
abDir[33] = (this.options[1] >> 16) & 255;
|
|
249
|
+
abDir[34] = (this.options[2] >> 24) & 255;
|
|
250
|
+
abDir[35] = (this.options[2] >> 16) & 255;
|
|
251
|
+
abDir[36] = (this.options[2] >> 8) & 255;
|
|
252
|
+
abDir[37] = this.options[2] & 255;
|
|
253
|
+
abDir[38] = array1[21];
|
|
254
|
+
abDir[39] = array1[22];
|
|
255
|
+
abDir[40] = array2[21];
|
|
256
|
+
abDir[41] = array2[22];
|
|
257
|
+
abDir[42] = array3[23];
|
|
258
|
+
abDir[43] = array3[24];
|
|
259
|
+
abDir[44] = (endEncryption >> 24) & 255;
|
|
260
|
+
abDir[45] = (endEncryption >> 16) & 255;
|
|
261
|
+
abDir[46] = (endEncryption >> 8) & 255;
|
|
262
|
+
abDir[47] = endEncryption & 255;
|
|
263
|
+
abDir[48] = abDir[8];
|
|
264
|
+
abDir[49] = Math.floor(endEncryption / 0x100000000);
|
|
265
|
+
abDir[50] = Math.floor(endEncryption / 0x10000000000);
|
|
266
|
+
abDir[51] = (this.pageId >> 24) & 255;
|
|
267
|
+
abDir[52] = (this.pageId >> 16) & 255;
|
|
268
|
+
abDir[53] = (this.pageId >> 8) & 255;
|
|
269
|
+
abDir[54] = this.pageId & 255;
|
|
270
|
+
abDir[55] = this.pageId;
|
|
271
|
+
abDir[56] = this.aid;
|
|
272
|
+
abDir[57] = this.aid & 255;
|
|
273
|
+
abDir[58] = (this.aid >> 8) & 255;
|
|
274
|
+
abDir[59] = (this.aid >> 16) & 255;
|
|
275
|
+
abDir[60] = (this.aid >> 24) & 255;
|
|
276
|
+
abDir[64] = this.browserFp.length;
|
|
277
|
+
abDir[65] = this.browserFp.length;
|
|
278
|
+
const sortedValues = this.sortIndex.map((i) => abDir[i] || 0);
|
|
279
|
+
const fpArray = StringProcessor.toCharArray(this.browserFp);
|
|
280
|
+
let abXor = 0;
|
|
281
|
+
this.sortIndex2.forEach((key, idx) => {
|
|
282
|
+
const val = abDir[key] || 0;
|
|
283
|
+
abXor = idx === 0 ? val : abXor ^ val;
|
|
284
|
+
});
|
|
285
|
+
const allValues = [...sortedValues, ...fpArray, abXor];
|
|
286
|
+
const transformedValues = this.cryptoUtility.transformBytes(allValues);
|
|
287
|
+
const randomPrefix = StringProcessor.generateRandomBytes(3)
|
|
288
|
+
.split("")
|
|
289
|
+
.map((c) => c.charCodeAt(0));
|
|
290
|
+
const finalValues = [...randomPrefix, ...transformedValues];
|
|
291
|
+
const abogus = this.cryptoUtility.abogusEncode(finalValues, 0);
|
|
292
|
+
const finalParams = `${params}&a_bogus=${abogus}`;
|
|
293
|
+
return [finalParams, abogus, this.userAgent, body];
|
|
294
|
+
}
|
|
295
|
+
}
|
package/lib/stream.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Recorder } from "@bililive-tools/manager";
|
|
2
|
-
export declare function getInfo(channelId: string
|
|
2
|
+
export declare function getInfo(channelId: string, opts?: {
|
|
3
|
+
cookie?: string;
|
|
4
|
+
api?: "web" | "webHTML";
|
|
5
|
+
}): Promise<{
|
|
3
6
|
living: boolean;
|
|
4
7
|
owner: string;
|
|
5
8
|
title: string;
|
|
@@ -15,6 +18,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" |
|
|
|
15
18
|
cookie?: string;
|
|
16
19
|
formatPriorities?: Array<"flv" | "hls">;
|
|
17
20
|
doubleScreen?: boolean;
|
|
21
|
+
api?: "web" | "webHTML";
|
|
18
22
|
}): Promise<{
|
|
19
23
|
currentStream: {
|
|
20
24
|
name: string;
|
package/lib/stream.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getRoomInfo } from "./douyin_api.js";
|
|
2
|
-
export async function getInfo(channelId) {
|
|
3
|
-
const info = await getRoomInfo(channelId);
|
|
2
|
+
export async function getInfo(channelId, opts) {
|
|
3
|
+
const info = await getRoomInfo(channelId, opts ?? {});
|
|
4
4
|
return {
|
|
5
5
|
living: info.living,
|
|
6
6
|
owner: info.owner,
|
|
@@ -14,9 +14,9 @@ export async function getInfo(channelId) {
|
|
|
14
14
|
}
|
|
15
15
|
export async function getStream(opts) {
|
|
16
16
|
const info = await getRoomInfo(opts.channelId, {
|
|
17
|
-
retryOnSpecialCode: true,
|
|
18
17
|
doubleScreen: opts.doubleScreen ?? true,
|
|
19
18
|
auth: opts.cookie,
|
|
19
|
+
api: opts.api ?? "web",
|
|
20
20
|
});
|
|
21
21
|
if (!info.living) {
|
|
22
22
|
throw new Error("It must be called getStream when living");
|
package/lib/utils.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export declare function assertStringType(data: unknown, msg?: string): asserts d
|
|
|
9
9
|
export declare function assertNumberType(data: unknown, msg?: string): asserts data is number;
|
|
10
10
|
export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
|
|
11
11
|
export declare function replaceExtName(filePath: string, newExtName: string): string;
|
|
12
|
+
export declare function get__ac_signature(one_time_stamp: any, one_site: any, one_nonce: any, ua_n: any): string;
|
package/lib/utils.js
CHANGED
|
@@ -42,3 +42,70 @@ export function assertObjectType(data, msg) {
|
|
|
42
42
|
export function replaceExtName(filePath, newExtName) {
|
|
43
43
|
return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath)) + newExtName);
|
|
44
44
|
}
|
|
45
|
+
export function get__ac_signature(one_time_stamp, one_site, one_nonce, ua_n) {
|
|
46
|
+
function cal_one_str(one_str, orgi_iv) {
|
|
47
|
+
var k = orgi_iv;
|
|
48
|
+
for (var i = 0; i < one_str.length; i++) {
|
|
49
|
+
var a = one_str.charCodeAt(i);
|
|
50
|
+
k = ((k ^ a) * 65599) >>> 0;
|
|
51
|
+
}
|
|
52
|
+
return k;
|
|
53
|
+
}
|
|
54
|
+
function cal_one_str_3(one_str, orgi_iv) {
|
|
55
|
+
// 用于计算后两位
|
|
56
|
+
var k = orgi_iv;
|
|
57
|
+
for (var i = 0; i < one_str.length; i++) {
|
|
58
|
+
k = (k * 65599 + one_str.charCodeAt(i)) >>> 0;
|
|
59
|
+
}
|
|
60
|
+
return k;
|
|
61
|
+
}
|
|
62
|
+
function get_one_chr(enc_chr_code) {
|
|
63
|
+
if (enc_chr_code < 26) {
|
|
64
|
+
return String.fromCharCode(enc_chr_code + 65);
|
|
65
|
+
}
|
|
66
|
+
else if (enc_chr_code < 52) {
|
|
67
|
+
return String.fromCharCode(enc_chr_code + 71);
|
|
68
|
+
}
|
|
69
|
+
else if (enc_chr_code < 62) {
|
|
70
|
+
return String.fromCharCode(enc_chr_code - 4);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
return String.fromCharCode(enc_chr_code - 17);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function enc_num_to_str(one_orgi_enc) {
|
|
77
|
+
var s = "";
|
|
78
|
+
for (var i = 24; i >= 0; i -= 6) {
|
|
79
|
+
s += get_one_chr((one_orgi_enc >> i) & 63);
|
|
80
|
+
}
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
// var time_stamp = 1740635781
|
|
84
|
+
// var site = 'www.douyin.com/'; 提取自window.location.href
|
|
85
|
+
// var data_url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAAAXNSR0IArs4c6QAAAV5JREFUSEvFlU1OAkEQhT9WLlyx5BqujTfhAMTbsNIYj+EpXLjyDCzhAgbSZMoUb6q6GwWGhMxM/8y8ej/VM05/e3kuj7NhrMzZfbBskqF9BMiAKmAt7trFRGR6Qsv9SQHZBt103DiRGiNSlcWIdW8h75NrK6Ce9M74nYsK0I1ZBnyxme0ytWpZy8IUqT7KQE0BA6MqeDCl2FZhmf1qttR3VhU4h3HPsKoQFaIkeAeEDA9IM7umITYmLcARy1GLrYHuAat5a+VsVIC3QM0KNvcCfAGvclb0WLHVQLxV08woK1GAvBo6/wk8A+Ua2cnWRx7uKcCAZ+dV82RtBesHuAPK1fs7A/eXblWz0b8zUCy0crJkjGXZUJtkbb1LgZ4u4tfsgHnWtC883p0BDbHvQjZXQG8b7e3C+PPXRdIsgE2yZQmsgUfg+2YoKx+KCngCPoD3IZz3wMPwfxPPT17DAcLKggYInDZeAAAAAElFTkSuQmCC'
|
|
86
|
+
// var ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0";
|
|
87
|
+
// var nonce = '067bffadf00143f576ddf';
|
|
88
|
+
// var result = '_02B4Z6wo00f01HBw-fgAAIDA-rdLmXfuMxxwUP1AAHurb5'
|
|
89
|
+
var sign_head = "_02B4Z6wo00f01", time_stamp_s = one_time_stamp + "";
|
|
90
|
+
var a = cal_one_str(one_site, cal_one_str(time_stamp_s, 0)) % 65521;
|
|
91
|
+
var b = parseInt("10000000110000" +
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
parseInt((one_time_stamp ^ (a * 65521)) >>> 0)
|
|
94
|
+
.toString(2)
|
|
95
|
+
.padStart(32, "0"), 2), b_s = b + "";
|
|
96
|
+
var c = cal_one_str(b_s, 0);
|
|
97
|
+
var d = enc_num_to_str(b >> 2);
|
|
98
|
+
var e = (b / 4294967296) >>> 0;
|
|
99
|
+
var f = enc_num_to_str((b << 28) | (e >>> 4));
|
|
100
|
+
var g = 582085784 ^ b;
|
|
101
|
+
var h = enc_num_to_str((e << 26) | (g >>> 6));
|
|
102
|
+
var i = get_one_chr(g & 63);
|
|
103
|
+
var j = (cal_one_str(ua_n, c) % 65521 << 16) | cal_one_str(one_nonce, c) % 65521;
|
|
104
|
+
var k = enc_num_to_str(j >> 2);
|
|
105
|
+
var l = enc_num_to_str((j << 28) | ((524576 ^ b) >>> 4));
|
|
106
|
+
var m = enc_num_to_str(a);
|
|
107
|
+
var n = sign_head + d + f + h + i + k + l + m;
|
|
108
|
+
var o = parseInt(cal_one_str_3(n, 0)).toString(16).slice(-2);
|
|
109
|
+
var signature = n + o;
|
|
110
|
+
return signature;
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/douyin-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "@bililive-tools douyin recorder implemention",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
"axios": "^1.7.8",
|
|
38
38
|
"lodash-es": "^4.17.21",
|
|
39
39
|
"mitt": "^3.0.1",
|
|
40
|
+
"sm-crypto": "^0.3.13",
|
|
40
41
|
"douyin-danma-listener": "0.2.0",
|
|
41
|
-
"@bililive-tools/manager": "^1.
|
|
42
|
+
"@bililive-tools/manager": "^1.6.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "*"
|