aiconductor-cli 1.7.0 → 1.8.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 +18 -1
- package/bin/aiconductor-cli.js +12 -1
- package/package.json +1 -1
- package/src/api.js +16 -0
- package/src/api.test.js +68 -0
- package/src/commands/douyin.js +78 -0
package/README.md
CHANGED
|
@@ -302,13 +302,30 @@ aiconductor-cli douyin link-to-text <视频URL> [选项]
|
|
|
302
302
|
aiconductor-cli douyin link-to-text "https://www.douyin.com/video/xxxxx"
|
|
303
303
|
```
|
|
304
304
|
|
|
305
|
+
#### 视频下载(普通清晰度)
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
aiconductor-cli douyin download <视频URL|分享文案|作品ID> [选项]
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
一次返回多档媒体直链(通常 4 条:`hd_no_watermark` / `no_watermark` / `watermark` / `audio`),带高清无水印、普通无水印、带水印与纯音频,各自带清晰度标签与文件大小。
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
aiconductor-cli douyin download "https://www.douyin.com/video/7696718696647036223" -f text
|
|
315
|
+
aiconductor-cli douyin download "https://v.douyin.com/xxxxx/"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
返回路径:`data.medias[]`(`url` / `quality` / `type` / `extension` / `data_size`,音频条目额外带 `duration` 秒),另有 `author` / `title` / `thumbnail` / `duration`。
|
|
319
|
+
|
|
320
|
+
> **备用方案不等于失败**:上游主链路失败时会走兜底,此时 `medias` 只剩 2 条(`No Watermark` + `Audio`)、`data_size` 为 0、`message` 含「(备用方案)」。
|
|
321
|
+
|
|
305
322
|
#### 原画下载
|
|
306
323
|
|
|
307
324
|
```bash
|
|
308
325
|
aiconductor-cli douyin video-original <视频URL|分享文案|作品ID> [选项]
|
|
309
326
|
```
|
|
310
327
|
|
|
311
|
-
拿最高画质(原画)直链与文件大小,和普通清晰度的 `
|
|
328
|
+
拿最高画质(原画)直链与文件大小,和普通清晰度的 `download` 是两个档位。
|
|
312
329
|
|
|
313
330
|
```bash
|
|
314
331
|
aiconductor-cli douyin video-original "https://www.douyin.com/video/7669664934044795354" -f text
|
package/bin/aiconductor-cli.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
linkToTextCommand,
|
|
21
21
|
topicRadarCommand,
|
|
22
22
|
videoOriginalCommand,
|
|
23
|
+
downloadCommand,
|
|
23
24
|
} = require("../src/commands/douyin");
|
|
24
25
|
const {
|
|
25
26
|
hotListCommand: xhsHotListCommand,
|
|
@@ -245,9 +246,19 @@ douyinCmd
|
|
|
245
246
|
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
246
247
|
.action((keyword, options) => { videoSearchCommand(keyword, options); });
|
|
247
248
|
|
|
249
|
+
douyinCmd
|
|
250
|
+
.command("download")
|
|
251
|
+
.description("抖音视频下载:普通清晰度多档直链(原画请用 video-original)")
|
|
252
|
+
.argument("<video-url>", "抖音链接、分享文案或作品 ID")
|
|
253
|
+
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
254
|
+
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
|
255
|
+
.action((videoUrl, options) => {
|
|
256
|
+
downloadCommand(videoUrl, options);
|
|
257
|
+
});
|
|
258
|
+
|
|
248
259
|
douyinCmd
|
|
249
260
|
.command("video-original")
|
|
250
|
-
.description("抖音原画下载:取最高画质播放直链(普通清晰度请用
|
|
261
|
+
.description("抖音原画下载:取最高画质播放直链(普通清晰度请用 download)")
|
|
251
262
|
.argument("<video-url>", "抖音链接、分享文案或作品 ID")
|
|
252
263
|
.option("-k, --api-key <key>", "AIConductor API Key(也可通过 AICONDUCTOR_API_KEY 环境变量或 .env 文件提供)")
|
|
253
264
|
.option("-f, --format <format>", "输出格式:json 或 text", "json")
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -23,6 +23,7 @@ const DOUYIN_HOT_WORDS_PATH = "/api/douyin_hot_words";
|
|
|
23
23
|
const DOUYIN_HOT_TOPICS_PATH = "/api/douyin_hot_topics";
|
|
24
24
|
const DOUYIN_RISING_TOPICS_PATH = "/api/douyin_rising_topics";
|
|
25
25
|
const DOUYIN_VIDEO_ORIGINAL_PATH = "/api/douyin_video_original";
|
|
26
|
+
const VIDEO_DOWNLOAD_URL_PATH = "/api/video_download_url";
|
|
26
27
|
const XHS_NOTE_PUBLISH_PATH = "/api/xhs_note_publish";
|
|
27
28
|
const XHS_NOTE_INFO_PATH = "/api/xhs_note_info_v3";
|
|
28
29
|
const XHS_NOTE_COMMENTS_PATH = "/api/xhs_note_comments_fast";
|
|
@@ -570,6 +571,20 @@ async function douyinVideoOriginal(apiKey, videoUrl) {
|
|
|
570
571
|
return stripBilling(resp);
|
|
571
572
|
}
|
|
572
573
|
|
|
574
|
+
/**
|
|
575
|
+
* 抖音视频下载(普通清晰度,多档媒体列表)
|
|
576
|
+
* @param {string} apiKey
|
|
577
|
+
* @param {string} videoUrl 链接、分享文案或作品 ID
|
|
578
|
+
* @returns {Promise<object>}
|
|
579
|
+
*/
|
|
580
|
+
async function douyinVideoDownload(apiKey, videoUrl) {
|
|
581
|
+
const resp = await httpPost(API_HOST, VIDEO_DOWNLOAD_URL_PATH, {
|
|
582
|
+
api_key: apiKey,
|
|
583
|
+
video_url: videoUrl,
|
|
584
|
+
});
|
|
585
|
+
return stripBilling(resp);
|
|
586
|
+
}
|
|
587
|
+
|
|
573
588
|
/**
|
|
574
589
|
* 抖音热词榜(原子接口)
|
|
575
590
|
* @param {string} apiKey
|
|
@@ -1042,6 +1057,7 @@ module.exports = {
|
|
|
1042
1057
|
douyinHotTopics,
|
|
1043
1058
|
douyinRisingTopics,
|
|
1044
1059
|
douyinVideoOriginal,
|
|
1060
|
+
douyinVideoDownload,
|
|
1045
1061
|
r2Upload,
|
|
1046
1062
|
xhsNotePublish,
|
|
1047
1063
|
xhsNoteInfo,
|
package/src/api.test.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
xhsPgyAudience,
|
|
17
17
|
xhsPgyPerformance,
|
|
18
18
|
douyinVideoOriginal,
|
|
19
|
+
douyinVideoDownload,
|
|
19
20
|
} = require("./api");
|
|
20
21
|
const {
|
|
21
22
|
xhsNoteInfo,
|
|
@@ -51,6 +52,8 @@ const {
|
|
|
51
52
|
buildTopicRadarResult,
|
|
52
53
|
formatTopicRadarText,
|
|
53
54
|
formatVideoOriginalText,
|
|
55
|
+
buildDownloadResult,
|
|
56
|
+
formatDownloadText,
|
|
54
57
|
} = require("./commands/douyin");
|
|
55
58
|
const {
|
|
56
59
|
wxArticleData,
|
|
@@ -1063,3 +1066,68 @@ test("formatDarenReportText renders profile, audience, performance and score", (
|
|
|
1063
1066
|
assert.match(text, /【评分】78 分 · 腰部(部分维度缺失,已归一)/);
|
|
1064
1067
|
assert.match(text, /失败数据源:/);
|
|
1065
1068
|
});
|
|
1069
|
+
|
|
1070
|
+
test("douyin download posts to the download endpoint and strips billing", async () => {
|
|
1071
|
+
const request = await captureRequest(
|
|
1072
|
+
() => douyinVideoDownload("test-key", "https://v.douyin.com/abc/"),
|
|
1073
|
+
{ success: true, message: "ok", medias: [{ url: "https://cdn/x.mp4" }], billing: { service_fee: 0.01 } }
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
assert.equal(request.options.path, "/api/video_download_url");
|
|
1077
|
+
assert.equal(request.options.method, "POST");
|
|
1078
|
+
assert.deepEqual(request.body, { api_key: "test-key", video_url: "https://v.douyin.com/abc/" });
|
|
1079
|
+
assert.equal(request.result.billing, undefined);
|
|
1080
|
+
assert.equal(request.result.medias.length, 1);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test("buildDownloadResult passes a successful payload through unchanged", () => {
|
|
1084
|
+
const resp = { success: true, medias: [{ url: "https://cdn/x.mp4" }] };
|
|
1085
|
+
assert.equal(buildDownloadResult(resp), resp);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
test("buildDownloadResult turns a business failure into api_error with the upstream message", () => {
|
|
1089
|
+
assert.throws(
|
|
1090
|
+
() => buildDownloadResult({ success: false, message: "账户余额不足,当前余额:0.0,需要:0.01", medias: [] }),
|
|
1091
|
+
(err) => err.type === "api_error" && /账户余额不足/.test(err.message)
|
|
1092
|
+
);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
test("formatDownloadText lists every media, sizes and the fallback caveat", () => {
|
|
1096
|
+
const text = formatDownloadText({
|
|
1097
|
+
success: true,
|
|
1098
|
+
message: "获取视频下载URL成功",
|
|
1099
|
+
source: "douyin",
|
|
1100
|
+
author: "某作者",
|
|
1101
|
+
title: "某条视频",
|
|
1102
|
+
duration: 33000,
|
|
1103
|
+
medias: [
|
|
1104
|
+
{ url: "https://cdn/hd.mp4", type: "video", quality: "hd_no_watermark", extension: "mp4", data_size: 19465218 },
|
|
1105
|
+
{ url: "https://cdn/audio.mp3", type: "audio", quality: "audio", extension: "mp3", duration: 33 },
|
|
1106
|
+
],
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
assert.match(text, /视频下载直链/);
|
|
1110
|
+
assert.match(text, /作者: 某作者/);
|
|
1111
|
+
assert.match(text, /标题: 某条视频/);
|
|
1112
|
+
assert.match(text, /hd_no_watermark/);
|
|
1113
|
+
assert.match(text, /18\.6 MB/);
|
|
1114
|
+
assert.match(text, /https:\/\/cdn\/hd\.mp4/);
|
|
1115
|
+
assert.match(text, /audio/);
|
|
1116
|
+
assert.match(text, /https:\/\/cdn\/audio\.mp3/);
|
|
1117
|
+
// 正常档音频本来就不带大小,不能误报成“备用方案”
|
|
1118
|
+
assert.doesNotMatch(text, /备用方案/);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
test("formatDownloadText explains the fallback shape instead of looking broken", () => {
|
|
1122
|
+
const text = formatDownloadText({
|
|
1123
|
+
success: true,
|
|
1124
|
+
message: "获取视频下载URL成功(备用方案)",
|
|
1125
|
+
medias: [
|
|
1126
|
+
{ url: "https://cdn/nw.mp4", type: "video", quality: "No Watermark", extension: "mp4", data_size: 0 },
|
|
1127
|
+
{ url: "https://cdn/a.mp3", type: "audio", quality: "Audio", extension: "mp3", duration: 15 },
|
|
1128
|
+
],
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
assert.match(text, /备用方案/);
|
|
1132
|
+
assert.doesNotMatch(text, /0 B/);
|
|
1133
|
+
});
|
package/src/commands/douyin.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
douyinHotTopics,
|
|
11
11
|
douyinRisingTopics,
|
|
12
12
|
douyinVideoOriginal,
|
|
13
|
+
douyinVideoDownload,
|
|
13
14
|
} = require("../api");
|
|
14
15
|
const { outputResult, resolveKeyOrExit, handleApiError, safeParseInt } = require("../lib/cli-utils");
|
|
15
16
|
|
|
@@ -250,6 +251,79 @@ async function videoOriginalCommand(videoUrl, options = {}) {
|
|
|
250
251
|
} catch (err) { handleApiError(err, format); process.exit(1); }
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
async function downloadCommand(videoUrl, options = {}) {
|
|
255
|
+
const format = options.format || "json";
|
|
256
|
+
const apiKey = resolveKeyOrExit(options.apiKey, format);
|
|
257
|
+
try {
|
|
258
|
+
const result = buildDownloadResult(await douyinVideoDownload(apiKey, videoUrl));
|
|
259
|
+
outputResult(result, format, formatDownloadText);
|
|
260
|
+
process.exit(0);
|
|
261
|
+
} catch (err) { handleApiError(err, format); process.exit(1); }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 业务失败(success:false,HTTP 200)要变成 api_error,否则会被当成退出码 0。
|
|
266
|
+
* @param {object} resp
|
|
267
|
+
* @returns {object}
|
|
268
|
+
*/
|
|
269
|
+
function buildDownloadResult(resp) {
|
|
270
|
+
if (resp && resp.success === false) {
|
|
271
|
+
throw Object.assign(
|
|
272
|
+
new Error(resp.message || "视频下载解析失败"),
|
|
273
|
+
{ type: "api_error", body: resp }
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return resp;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 字节数转人类可读;0 / 空值(备用方案拿不到大小)返回空串,不打印 "0 B"。
|
|
281
|
+
* @param {number} bytes
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
function formatFileSize(bytes) {
|
|
285
|
+
const n = Number(bytes);
|
|
286
|
+
if (!Number.isFinite(n) || n <= 0) return "";
|
|
287
|
+
if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
288
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
289
|
+
return `${n} B`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* text 输出:视频下载直链(多档媒体列表)
|
|
294
|
+
* @param {object} result
|
|
295
|
+
* @returns {string}
|
|
296
|
+
*/
|
|
297
|
+
function formatDownloadText(result = {}) {
|
|
298
|
+
const medias = Array.isArray(result.medias) ? result.medias : [];
|
|
299
|
+
const lines = ["✓ 抖音视频下载直链"];
|
|
300
|
+
if (result.message) lines.push(` ${result.message}`);
|
|
301
|
+
|
|
302
|
+
const meta = [];
|
|
303
|
+
if (result.author) meta.push(`作者: ${result.author}`);
|
|
304
|
+
if (result.source) meta.push(`平台: ${result.source}`);
|
|
305
|
+
if (meta.length) lines.push(` ${meta.join(" · ")}`);
|
|
306
|
+
if (result.title) lines.push(` 标题: ${result.title}`);
|
|
307
|
+
|
|
308
|
+
lines.push(` 共 ${medias.length} 个文件:`);
|
|
309
|
+
medias.forEach((item = {}, index) => {
|
|
310
|
+
const parts = [item.quality || "unknown"];
|
|
311
|
+
const kind = [item.type, item.extension].filter(Boolean).join("/");
|
|
312
|
+
if (kind) parts.push(kind);
|
|
313
|
+
const size = formatFileSize(item.data_size);
|
|
314
|
+
if (size) parts.push(size);
|
|
315
|
+
lines.push(` ${index + 1}. ${parts.join(" · ")}`);
|
|
316
|
+
if (item.url) lines.push(` ${item.url}`);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const fallback = typeof result.message === "string" && result.message.includes("备用方案");
|
|
320
|
+
if (fallback) {
|
|
321
|
+
lines.push(" 提示: 备用方案只给无水印视频与音频,拿不到文件大小");
|
|
322
|
+
}
|
|
323
|
+
lines.push(" 提示: 直链有时效,请立即下载");
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
253
327
|
/**
|
|
254
328
|
* text 输出:原画直链
|
|
255
329
|
* @param {object} result
|
|
@@ -280,4 +354,8 @@ module.exports = {
|
|
|
280
354
|
formatTopicRadarText,
|
|
281
355
|
videoOriginalCommand,
|
|
282
356
|
formatVideoOriginalText,
|
|
357
|
+
downloadCommand,
|
|
358
|
+
buildDownloadResult,
|
|
359
|
+
formatDownloadText,
|
|
360
|
+
formatFileSize,
|
|
283
361
|
};
|