fix(baoyu-fetch): extract X video variants

This commit is contained in:
Jim Liu 宝玉
2026-04-21 13:39:01 -05:00
parent d7f623158c
commit fe26c2b639
4 changed files with 261 additions and 22 deletions
+48 -20
View File
@@ -9,18 +9,43 @@ import {
getUser,
isRecord,
normalizeTitle,
resolveBestXVideoVariantUrl,
toHighResXImageUrl,
toXTweet,
} from "./shared";
import type { JsonObject } from "./types";
function resolveArticleMediaUrl(mediaInfo: JsonObject): string {
interface ArticleMedia {
kind: "image" | "video";
url: string;
}
function resolveArticleMedia(mediaInfo: JsonObject): ArticleMedia | null {
const videoUrl = resolveBestXVideoVariantUrl(mediaInfo);
if (videoUrl) {
return {
kind: "video",
url: videoUrl,
};
}
const rawUrl =
(typeof mediaInfo.original_img_url === "string" && mediaInfo.original_img_url) ||
(typeof mediaInfo.url === "string" && mediaInfo.url) ||
"";
return rawUrl ? toHighResXImageUrl(rawUrl) : "";
if (!rawUrl) {
return null;
}
return {
kind: "image",
url: toHighResXImageUrl(rawUrl),
};
}
function resolveArticleMediaUrl(mediaInfo: JsonObject): string {
return resolveArticleMedia(mediaInfo)?.url ?? "";
}
function normalizeEntityMap(entityMap: unknown): Map<string, JsonObject> {
@@ -139,8 +164,8 @@ function getTweetId(entityMap: Map<string, JsonObject>, entityKey: unknown): str
return data.tweetId;
}
function buildMediaUrlMap(articleResult: JsonObject): Map<string, string> {
const mediaMap = new Map<string, string>();
function buildMediaMap(articleResult: JsonObject): Map<string, ArticleMedia> {
const mediaMap = new Map<string, ArticleMedia>();
const mediaEntities = Array.isArray(articleResult.media_entities) ? articleResult.media_entities : [];
for (const entity of mediaEntities) {
@@ -148,25 +173,28 @@ function buildMediaUrlMap(articleResult: JsonObject): Map<string, string> {
continue;
}
const mediaInfo = entity.media_info;
const url = resolveArticleMediaUrl(mediaInfo);
if (url) {
mediaMap.set(entity.media_id, url);
const media = resolveArticleMedia(entity.media_info);
if (media) {
mediaMap.set(entity.media_id, media);
}
}
const coverMedia = isRecord(articleResult.cover_media) ? articleResult.cover_media : null;
if (coverMedia && typeof coverMedia.media_id === "string" && isRecord(coverMedia.media_info)) {
const url = resolveArticleMediaUrl(coverMedia.media_info);
if (url) {
mediaMap.set(coverMedia.media_id, url);
const media = resolveArticleMedia(coverMedia.media_info);
if (media) {
mediaMap.set(coverMedia.media_id, media);
}
}
return mediaMap;
}
function getMediaMarkdown(entityMap: Map<string, JsonObject>, entityKey: unknown, mediaMap: Map<string, string>): string[] {
function getMediaMarkdown(
entityMap: Map<string, JsonObject>,
entityKey: unknown,
mediaMap: Map<string, ArticleMedia>,
): string[] {
const key =
typeof entityKey === "string" || typeof entityKey === "number"
? String(entityKey)
@@ -182,19 +210,19 @@ function getMediaMarkdown(entityMap: Map<string, JsonObject>, entityKey: unknown
const data = isRecord(entity.data) ? entity.data : {};
const mediaItems = Array.isArray(data.mediaItems) ? data.mediaItems : [];
const urls: string[] = [];
const media: ArticleMedia[] = [];
for (const item of mediaItems) {
if (!isRecord(item) || typeof item.mediaId !== "string") {
continue;
}
const url = mediaMap.get(item.mediaId);
if (url && !urls.includes(url)) {
urls.push(url);
const mediaItem = mediaMap.get(item.mediaId);
if (mediaItem && !media.some((value) => value.url === mediaItem.url)) {
media.push(mediaItem);
}
}
return urls.map((url) => `![](${url})`);
return media.map((item) => item.kind === "image" ? `![](${item.url})` : `[video](${item.url})`);
}
function resolveTweetMarkdown(payloads: unknown[], tweetId: string, pageUrl: string): string | null {
@@ -250,7 +278,7 @@ function replaceLinkEntities(text: string, block: JsonObject, entityMap: Map<str
function renderAtomicBlock(
block: JsonObject,
entityMap: Map<string, JsonObject>,
mediaMap: Map<string, string>,
mediaMap: Map<string, ArticleMedia>,
payloads: unknown[],
pageUrl: string,
): string | null {
@@ -293,7 +321,7 @@ function renderAtomicBlock(
function renderArticleBlocks(
blocks: unknown[],
entityMap: Map<string, JsonObject>,
mediaMap: Map<string, string>,
mediaMap: Map<string, ArticleMedia>,
payloads: unknown[],
pageUrl: string,
): string {
@@ -396,7 +424,7 @@ export function extractArticleDocumentFromPayload(
const contentState = isRecord(articleResult.content_state) ? articleResult.content_state : {};
const blocks = Array.isArray(contentState.blocks) ? contentState.blocks : [];
const entityMap = normalizeEntityMap(contentState.entityMap);
const mediaMap = buildMediaUrlMap(articleResult);
const mediaMap = buildMediaMap(articleResult);
const richMarkdown = renderArticleBlocks(blocks, entityMap, mediaMap, payloads, pageUrl);
const plainText = typeof articleResult.plain_text === "string" ? articleResult.plain_text.trim() : "";
const markdown = richMarkdown || plainText || getTweetText(tweet);
+39 -2
View File
@@ -251,6 +251,39 @@ export function toHighResXImageUrl(rawUrl: string): string {
}
}
function getVideoVariantBitrate(variant: JsonObject): number {
const value = variant.bitrate ?? variant.bit_rate;
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function getVideoVariantContentType(variant: JsonObject): string {
const value = variant.content_type ?? variant.contentType;
return typeof value === "string" ? value.toLowerCase() : "";
}
export function resolveBestXVideoVariantUrl(mediaInfo: unknown): string | undefined {
if (!isRecord(mediaInfo)) {
return undefined;
}
const variantsSource =
Array.isArray(mediaInfo.variants)
? mediaInfo.variants
: isRecord(mediaInfo.video_info) && Array.isArray(mediaInfo.video_info.variants)
? mediaInfo.video_info.variants
: [];
const variants = variantsSource
.filter(
(variant): variant is JsonObject =>
isRecord(variant) && typeof variant.url === "string" && variant.url.length > 0,
)
.filter((variant) => getVideoVariantContentType(variant) === "video/mp4")
.sort((left, right) => getVideoVariantBitrate(right) - getVideoVariantBitrate(left));
return typeof variants[0]?.url === "string" ? variants[0].url : undefined;
}
export function getTweetMedia(tweet: JsonObject): XMedia[] {
const legacy = getLegacy(tweet);
const extendedEntities = isRecord(legacy.extended_entities) ? legacy.extended_entities : emptyObject();
@@ -268,10 +301,14 @@ export function getTweetMedia(tweet: JsonObject): XMedia[] {
alt: typeof value.ext_alt_text === "string" ? value.ext_alt_text : undefined,
};
}
if ((value.type === "video" || value.type === "animated_gif") && typeof value.media_url_https === "string") {
if (value.type === "video" || value.type === "animated_gif") {
const videoUrl = resolveBestXVideoVariantUrl(value);
if (!videoUrl) {
return null;
}
return {
type: value.type,
url: value.media_url_https,
url: videoUrl,
};
}
return null;