Spaces:
Running
Running
File size: 6,415 Bytes
461222a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | import { request } from "@/api/controllers/core.ts";
import logger from "@/lib/logger.ts";
/**
* 图片URL提取工具
* 统一从不同格式的响应中提取图片URL
*/
/**
* 从API响应项中提取图片URL
* @param item API响应中的单个项目
* @param index 项目索引(用于日志)
* @returns 图片URL或null
*/
export function extractImageUrl(item: any, index?: number): string | null {
const logPrefix = index !== undefined ? `图片 ${index + 1}` : '图片';
// 只提取 large_images
if (item?.image?.large_images?.[0]?.image_url) {
let imageUrl = item.image.large_images[0].image_url;
// 将URL中的 \u0026 转换为 &
imageUrl = imageUrl.replace(/\\u0026/g, '&');
logger.debug(`${logPrefix}: 使用 large_images URL`);
return imageUrl;
}
// 无法提取URL,记录警告
logger.warn(`${logPrefix}: 无法提取URL,缺少 image.large_images[0].image_url 字段。item结构: ${JSON.stringify(item, null, 2)}`);
return null;
}
/**
* 从项目列表中批量提取图片URLs
* @param itemList 项目列表
* @returns 图片URL数组
*/
export function extractImageUrls(itemList: any[]): string[] {
return itemList
.map((item, index) => extractImageUrl(item, index))
.filter((url): url is string => url !== null);
}
/**
* 从视频响应项中提取视频URL
* @param item 视频响应项
* @returns 视频URL或null
*/
export function extractVideoUrl(item: any): string | null {
// 优先尝试 common_attr.transcoded_video (亚太站结构)
if (item?.common_attr?.transcoded_video?.origin?.video_url) {
return item.common_attr.transcoded_video.origin.video_url;
}
// 尝试 video.transcoded_video (国内站结构)
if (item?.video?.transcoded_video?.origin?.video_url) {
return item.video.transcoded_video.origin.video_url;
}
// 尝试 play_url
if (item?.video?.play_url) {
return item.video.play_url;
}
// 尝试 download_url
if (item?.video?.download_url) {
return item.video.download_url;
}
// 尝试 url
if (item?.video?.url) {
return item.video.url;
}
return null;
}
/**
* 通过 get_local_item_list API 获取高质量视频下载URL
* 浏览器下载视频时使用此API获取高码率版本(~6297 vs 预览版 ~1152)
*
* @param itemId 视频项目ID
* @param refreshToken 刷新令牌
* @returns 高质量视频URL,失败时返回 null
*/
export async function fetchHighQualityVideoUrl(itemId: string, refreshToken: string): Promise<string | null> {
try {
logger.info(`尝试获取高质量视频下载URL,item_id: ${itemId}`);
const result = await request("post", "/mweb/v1/get_local_item_list", refreshToken, {
data: {
item_id_list: [itemId],
pack_item_opt: {
scene: 1,
need_data_integrity: true,
},
is_for_video_download: true,
},
});
const responseStr = JSON.stringify(result);
logger.info(`get_local_item_list 响应大小: ${responseStr.length} 字符`);
// 策略0(最优先): 从 video_model 字段提取无水印视频URL
// video_model 是 JSON 字符串,其中 video_list.*.main_url 为 Base64 编码的无水印链接
const itemList = result.item_list || result.local_item_list || [];
if (itemList.length > 0) {
const item = itemList[0];
const videoModelStr = item?.video?.video_model;
if (videoModelStr && typeof videoModelStr === "string") {
try {
const videoModel = JSON.parse(videoModelStr);
const videoList = videoModel?.video_list;
if (videoList && typeof videoList === "object") {
// 按清晰度优先级尝试提取:
// 4K(video_4) > 2K(video_3) > 1080p(video_2) > 720p(video_1)
// 目前只抓包到video_1
const priorityKeys = ["video_4", "video_3", "video_2", "video_1"];
for (const key of priorityKeys) {
const mainUrlB64 = videoList[key]?.main_url;
if (mainUrlB64 && typeof mainUrlB64 === "string") {
// Base64 解码得到真实无水印URL(Node.js 16+ 全局支持 atob)
const decodedUrl = atob(mainUrlB64);
if (decodedUrl.startsWith("http")) {
const definition = videoList[key]?.definition || key;
logger.info(`从video_model提取到无水印视频URL (${definition}): ${decodedUrl}`);
return decodedUrl;
}
}
}
}
} catch (e) {
logger.warn(`解析video_model JSON失败: ${e.message}`);
}
}
}
// 策略1: 从结构化字段中提取视频URL(可能带水印,作为降级方案)
if (itemList.length > 0) {
const item = itemList[0];
const videoUrl =
item?.common_attr?.transcoded_video?.origin?.video_url ||
item?.video?.transcoded_video?.origin?.video_url ||
item?.video?.download_url ||
item?.video?.play_url ||
item?.video?.url;
if (videoUrl) {
logger.info(`从get_local_item_list结构化字段获取到高清视频URL(可能含水印): ${videoUrl}`);
return videoUrl;
}
}
// 策略2: 正则匹配 dreamnia.jimeng.com 高质量URL
const hqUrlMatch = responseStr.match(/https:\/\/v[0-9]+-dreamnia\.jimeng\.com\/[^"\s\\]+/);
if (hqUrlMatch && hqUrlMatch[0]) {
logger.info(`正则提取到高质量视频URL (dreamnia): ${hqUrlMatch[0]}`);
return hqUrlMatch[0];
}
// 策略3: 匹配任何 jimeng.com 域名的视频URL
const jimengUrlMatch = responseStr.match(/https:\/\/v[0-9]+-[^"\\]*\.jimeng\.com\/[^"\s\\]+/);
if (jimengUrlMatch && jimengUrlMatch[0]) {
logger.info(`正则提取到jimeng视频URL: ${jimengUrlMatch[0]}`);
return jimengUrlMatch[0];
}
// 策略4: 匹配任何视频URL(兜底)
const anyVideoUrlMatch = responseStr.match(/https:\/\/v[0-9]+-[^"\\]*\.(vlabvod|jimeng)\.com\/[^"\s\\]+/);
if (anyVideoUrlMatch && anyVideoUrlMatch[0]) {
logger.info(`从get_local_item_list提取到视频URL: ${anyVideoUrlMatch[0]}`);
return anyVideoUrlMatch[0];
}
logger.warn(`未能从get_local_item_list响应中提取到视频URL`);
return null;
} catch (error) {
logger.warn(`获取高质量视频下载URL失败: ${error.message}`);
return null;
}
}
|