From 2d1c6cc2e9b2690f2bfbc29f3ac84288b3e98cf4 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 6 Nov 2024 09:35:20 -0800 Subject: [PATCH] feat(route): toutiao video (#17479) --- lib/routes/toutiao/templates/video.art | 7 ++ lib/routes/toutiao/types.ts | 133 +++++++++++++++++++++++-- lib/routes/toutiao/user.ts | 79 ++++++++++++--- 3 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 lib/routes/toutiao/templates/video.art diff --git a/lib/routes/toutiao/templates/video.art b/lib/routes/toutiao/templates/video.art new file mode 100644 index 00000000000000..1b007c35c256e7 --- /dev/null +++ b/lib/routes/toutiao/templates/video.art @@ -0,0 +1,7 @@ + diff --git a/lib/routes/toutiao/types.ts b/lib/routes/toutiao/types.ts index a422596ac6cae0..e3ede3b0d650a9 100644 --- a/lib/routes/toutiao/types.ts +++ b/lib/routes/toutiao/types.ts @@ -12,13 +12,19 @@ export interface Feed { cell_ctrls: CellCtrls; cell_flag: number; cell_layout_style: number; + /** + * 0: video + * 32: text (w) + * 49: video + * 60: article + */ cell_type: number; comment_count: number; common_raw_data: string; /** * Appears only if cell_type is 32 */ - content?: string; + content: string; content_decoration: string; control_meta: ControlMeta; cursor: number; @@ -55,7 +61,7 @@ export interface Feed { /** * Appears only if cell_type is 32 */ - rich_content?: string; + rich_content: string; read_count: number; reback_flag: number; repin_count: number; @@ -70,15 +76,22 @@ export interface Feed { title: string; url: string; /** - * Appears only if cell_type is 32 + * Appears only if cell_type is 32, 49(0) */ - user?: UserInfo; + user: UserInfoCell32 | UserInfoCell49; user_bury: number; user_digg: number; - user_info?: UserInfo; + /** + * Appears only if cell_type is 0, 60 + */ + user_info: UserInfoCell60; user_like: number; user_repin: number; user_repin_time: number; + /** + * Appears only if cell_type is 0, 49 + */ + video: Video; video_duration: number; video_style: number; image_list: ImageListItem[]; @@ -251,6 +264,62 @@ interface UserInteraction { type VideoInfo = unknown; +interface Video { + bitrate: number; + codec_type: string; + definition: string; + download_addr: DownloadAddr; + duration: number; + encode_user_tag: string; + file_hash: string; + height: number; + origin_cover: OriginCover; + play_addr: PlayAddr; + play_addr_list: PlayAddrListItem[]; + ratio: string; + size: number; + url_expire: number; + video_id: string; + volume: Volume; + vtype: string; + width: number; +} + +interface DownloadAddr { + uri: string; + url_list: string[]; +} + +interface OriginCover { + uri: string; + url_list: string[]; +} + +interface PlayAddr { + uri: string; + url_list: string[]; +} + +interface PlayAddrListItem { + bitrate: number; + codec_type: string; + definition: string; + encode_user_tag: string; + file_hash: string; + play_url_list: string[]; + quality: string; + size: number; + url_expire: number; + video_quality: number; + volume: Volume; + vtype: string; +} + +interface Volume { + Loudness: number; + Peak: number; +} + interface LogPb { cell_layout_style: string; group_id_str: string; @@ -267,10 +336,60 @@ interface ShowMore { url: string; } -interface UserInfo { +interface UserInfoCell32 { avatar_url: string; - description: string; desc: string; + id: number; + is_blocked: number; + is_blocking: number; + is_followed: number; + is_following: number; + is_friend: number; + live_info_type: number; + medals: unknown; + name: string; + remark_name: string; + schema: string; + screen_name: string; + theme_day: string; + user_auth_info: string; + user_decoration: string; + user_id: string; + user_verified: number; + verified_content: string; +} + +interface UserInfoCell49 { + info: { + avatar_uri: string; + avatar_url: string; + ban_status: boolean; + desc: string; + media_id: string; + name: string; + origin_profile_url: boolean; + origin_user_id: string; + schema: string; + user_auth_info: string; + user_id: string; + user_verified: number; + verified_content: string; + }; + relation: { + is_followed: number; + is_following: number; + is_friend: number; + }; + relation_count: { + followers_count: number; + following_count: number; + }; + user_id: string; +} + +interface UserInfoCell60 { + avatar_url: string; + description: string; follow: boolean; name: string; user_auth_info: string; diff --git a/lib/routes/toutiao/user.ts b/lib/routes/toutiao/user.ts index a6418bcd769f2e..21c66876eec124 100644 --- a/lib/routes/toutiao/user.ts +++ b/lib/routes/toutiao/user.ts @@ -7,6 +7,11 @@ import { generate_a_bogus } from './a-bogus'; import { Feed } from './types'; import RejectError from '@/errors/types/reject'; import { config } from '@/config'; +import path from 'node:path'; +import { getCurrentPath } from '@/utils/helpers'; +import { art } from '@/utils/render'; + +const __dirname = getCurrentPath(import.meta.url); export const route: Route = { path: '/user/token/:token', @@ -52,23 +57,71 @@ async function handler(ctx) { } const items = feed.map((item) => { - const enclosure = item.large_image_list?.pop(); - return { - title: item.title, - description: item.rich_content ?? item.abstract ?? item.content, - link: `https://www.toutiao.com/${item.cell_type === 60 ? 'article' : /* 32 */ 'w'}/${item.id}/`, - pubDate: parseDate(item.publish_time, 'X'), - author: item.user_info?.name ?? item.user?.name ?? item.source, - enclosure_url: enclosure?.url, - enclosure_type: enclosure?.url ? `image/${new URL(enclosure.url).pathname.split('.').pop()}` : undefined, - }; + switch (item.cell_type) { + case 0: + case 49: { + const video = item.video.play_addr_list.sort((a, b) => b.bitrate - a.bitrate)[0]; + return { + title: item.title, + description: art(path.join(__dirname, 'templates', 'video.art'), { + poster: item.video.origin_cover.url_list[0], + url: item.video.play_addr_list.sort((a, b) => b.bitrate - a.bitrate)[0].play_url_list[0], + }), + link: `https://www.toutiao.com/video/${item.id}/`, + pubDate: parseDate(item.publish_time, 'X'), + author: item.user?.info.name ?? item.source, + enclosure_url: video?.play_url_list[0], + enclosure_type: video?.play_url_list[0] ? 'video/mp4' : undefined, + user: { + name: item.user?.info.name, + avatar: item.user?.info.avatar_url, + description: item.user?.info.desc, + }, + }; + } + + // text w/o title + case 32: { + const enclosure = item.large_image_list?.pop(); + return { + title: item.content?.split('\n')[0], + description: item.rich_content, + link: `https://www.toutiao.com/w/${item.id}/`, + pubDate: parseDate(item.publish_time, 'X'), + author: item.user?.name, + enclosure_url: enclosure?.url, + enclosure_type: enclosure?.url ? `image/${new URL(enclosure.url).pathname.split('.').pop()}` : undefined, + user: { + name: item.user?.name, + avatar: item.user?.avatar_url, + description: item.user?.desc, + }, + }; + } + + // text w/ title + case 60: + default: + return { + title: item.title, + description: item.abstract, + link: `https://www.toutiao.com/article/${item.id}/`, + pubDate: parseDate(item.publish_time, 'X'), + author: item.user_info?.name, + user: { + name: item.user_info?.name, + avatar: item.user_info?.avatar_url, + description: item.user_info?.description, + }, + }; + } }); return { - title: `${feed[0].user_info?.name ?? feed[0].user?.name ?? feed[0].source}的头条主页 - 今日头条(www.toutiao.com)`, - description: feed[0].user_info?.description ?? feed[0].user?.desc, + title: `${items[0].user.name}的头条主页 - 今日头条(www.toutiao.com)`, + description: items[0].user.description, link: `https://www.toutiao.com/c/user/token/${token}/`, - image: feed[0].user_info?.avatar_url ?? feed[0].user?.avatar_url, + image: items[0].user.avatar, item: items, }; }