diff --git a/src/lib/videoUtils.ts b/src/lib/videoUtils.ts new file mode 100644 index 0000000..681bd08 --- /dev/null +++ b/src/lib/videoUtils.ts @@ -0,0 +1,185 @@ +/** + * 视频工具函数 + * 处理视频链接解析、时长格式化等 + */ + +/** + * 视频平台类型 + */ +export type VideoPlatform = 'bilibili' | 'youtube' | 'douyin' | 'other'; + +/** + * 视频解析信息 + */ +export interface VideoInfo { + platform: VideoPlatform; + videoId: string; + embedUrl: string; +} + +/** + * 解析视频链接,提取平台和视频ID + * @param url 视频链接 + * @returns 视频信息,解析失败返回 null + */ +export function parseVideoUrl(url: string): VideoInfo | null { + if (!url) return null; + + try { + // B站视频解析 - av号格式 + // 格式: bilibili.com/video/av123456 或 www.bilibili.com/video/av123456 + const bilibiliAvMatch = url.match(/bilibili\.com\/video\/av(\d+)/i); + if (bilibiliAvMatch) { + const aid = bilibiliAvMatch[1]; + return { + platform: 'bilibili', + videoId: `av${aid}`, + embedUrl: `//player.bilibili.com/player.html?aid=${aid}&high_quality=1&danmaku=0`, + }; + } + + // B站视频解析 - BV号格式 + // 格式: bilibili.com/video/BV1xx411c7mD + const bilibiliBvMatch = url.match(/bilibili\.com\/video\/(BV[\w]+)/i); + if (bilibiliBvMatch) { + const bvid = bilibiliBvMatch[1]; + return { + platform: 'bilibili', + videoId: bvid, + embedUrl: `//player.bilibili.com/player.html?bvid=${bvid}&high_quality=1&danmaku=0`, + }; + } + + // YouTube 视频解析 + // 格式: youtube.com/watch?v=xxx 或 youtu.be/xxx + const youtubeMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/); + if (youtubeMatch) { + const videoId = youtubeMatch[1]; + return { + platform: 'youtube', + videoId, + embedUrl: `https://www.youtube.com/embed/${videoId}`, + }; + } + + // 抖音视频解析 (预留) + // 格式: douyin.com/video/xxx + const douyinMatch = url.match(/douyin\.com\/video\/(\d+)/); + if (douyinMatch) { + return { + platform: 'douyin', + videoId: douyinMatch[1], + embedUrl: '', // 抖音暂不支持嵌入 + }; + } + + return null; + } catch { + return null; + } +} + +/** + * 格式化视频时长 + * @param seconds 秒数(字符串或数字) + * @returns 格式化后的时长 (如 "2:34" 或 "1:02:34") + */ +export function formatDuration(seconds: string | number): string { + const totalSeconds = typeof seconds === 'string' ? parseInt(seconds, 10) : seconds; + + if (isNaN(totalSeconds) || totalSeconds < 0) { + return '--:--'; + } + + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const secs = totalSeconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +/** + * 获取视频平台显示名称 + * @param platform 平台标识 + * @returns 平台中文名称 + */ +export function getPlatformName(platform: VideoPlatform): string { + const names: Record = { + bilibili: 'B站', + youtube: 'YouTube', + douyin: '抖音', + other: '视频', + }; + return names[platform] || '视频'; +} + +/** + * 获取视频平台主题色 + * @param platform 平台标识 + * @returns 平台主题色 (hex) + */ +export function getPlatformColor(platform: VideoPlatform): string { + const colors: Record = { + bilibili: '#fb7299', + youtube: '#ff0000', + douyin: '#000000', + other: '#6b7280', + }; + return colors[platform] || '#6b7280'; +} + +/** + * 检测视频链接的平台 + * @param url 视频链接 + * @returns 平台标识 + */ +export function detectPlatform(url: string): VideoPlatform { + if (!url) return 'other'; + + if (url.includes('bilibili.com')) return 'bilibili'; + if (url.includes('youtube.com') || url.includes('youtu.be')) return 'youtube'; + if (url.includes('douyin.com')) return 'douyin'; + + return 'other'; +} + +/** + * 检查视频是否支持嵌入播放 + * @param platform 平台标识 + * @returns 是否支持嵌入 + */ +export function supportsEmbed(platform: VideoPlatform): boolean { + return platform === 'bilibili' || platform === 'youtube'; +} + +/** + * 生成带选集参数的嵌入 URL + * @param url 原始视频链接 + * @param page 选集编号(从1开始) + * @returns 嵌入 URL + */ +export function getEmbedUrlWithPage(url: string, page: number = 1): string | null { + const videoInfo = parseVideoUrl(url); + if (!videoInfo) return null; + + if (videoInfo.platform === 'bilibili') { + // B站嵌入链接支持 p 参数指定分P + return `${videoInfo.embedUrl}&p=${page}`; + } + + // 其他平台暂不支持选集 + return videoInfo.embedUrl; +} + +/** + * 检查平台是否支持选集 + * @param platform 平台标识 + * @returns 是否支持选集 + */ +export function supportsEpisodes(platform: VideoPlatform): boolean { + return platform === 'bilibili'; +} diff --git a/src/types/index.ts b/src/types/index.ts index 9bec5c5..eb2cdcd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -36,6 +36,8 @@ export interface Message { usedTools?: string[]; /** 搜索到的图片(图片搜索工具结果) */ searchImages?: SearchImageData[]; + /** 搜索到的视频(视频搜索工具结果) */ + searchVideos?: SearchVideoData[]; } // 搜索图片数据类型 @@ -49,6 +51,28 @@ export interface SearchImageData { sourceUrl?: string; } +// 搜索视频数据类型 +export interface SearchVideoData { + /** 视频标题 */ + title: string; + /** 视频链接 */ + link: string; + /** 视频摘要 */ + snippet: string; + /** 相关度评分 */ + score: string; + /** 排序位置 */ + position: number; + /** 作者列表 */ + authors: string[]; + /** 发布日期 */ + date: string; + /** 时长(秒) */ + duration: string; + /** 封面图URL */ + coverImage: string; +} + // 工具调用记录 export interface ToolCall { id: string;