@@ -11,6 +11,11 @@ const MICROLINK_API = 'https://api.microlink.io'
1111const BILIBILI_API = 'https://api.bilibili.com/x/web-interface/view'
1212const CORS_PROXY = 'https://api.allorigins.win/raw?url='
1313
14+ // noembed — oEmbed 包装器,浏览器原生 fetch CORS 友好
15+ // 支持 YouTube / Vimeo / Twitter / SoundCloud / Vine / Flickr / Slideshare 等 50+ 平台
16+ // 参考 ~/.claude/skills/absorber-youtube/helpers.py 的 fetch_video_metadata 思路(浏览器版替代 yt-dlp)
17+ const NOEMBED_API = 'https://noembed.com/embed'
18+
1419/**
1520 * 获取域名 favicon URL
1621 * @param {string } url - 页面 URL
@@ -82,6 +87,44 @@ async function fetchBilibiliMetadata(url) {
8287 }
8388}
8489
90+ /**
91+ * 通过 noembed (oEmbed 协议) 拉视频元数据
92+ * 参考 absorber-youtube `fetch_video_metadata` 的等价浏览器版本
93+ * 支持 YouTube / Vimeo / TikTok / 抖音 / Twitter / SoundCloud 等
94+ * @param {string } url - 视频页面 URL
95+ * @returns {Promise<Object|null> } - 元数据或 null
96+ */
97+ async function fetchOembedMetadata ( url ) {
98+ try {
99+ const resp = await fetch ( `${ NOEMBED_API } ?url=${ encodeURIComponent ( url ) } ` )
100+ if ( ! resp . ok ) return null
101+ const data = await resp . json ( )
102+ // noembed 解析失败时会回 { error: '...' },没有 title 也算失败
103+ if ( ! data || data . error || ! data . title ) return null
104+
105+ // 缩略图:noembed 给 thumbnail_url
106+ let thumb = data . thumbnail_url || ''
107+ // YouTube 的高清缩略图:把 hqdefault.jpg 升级到 maxresdefault.jpg(如果可用)
108+ if ( thumb && thumb . includes ( 'ytimg.com' ) ) {
109+ thumb = thumb . replace ( / \/ ( h q d e f a u l t | m q d e f a u l t | s d d e f a u l t ) \. j p g / , '/maxresdefault.jpg' )
110+ }
111+
112+ // 描述:noembed 通常没 description,用 author_name 兜底
113+ const desc = data . author_name ? `作者:${ data . author_name } ` : ''
114+
115+ return {
116+ title : data . title ,
117+ description : desc ,
118+ image : thumb ,
119+ favicon : getFaviconUrl ( url ) ,
120+ screenshot : thumb ,
121+ }
122+ } catch ( e ) {
123+ console . warn ( 'noembed 抓取失败:' , e ?. message || e )
124+ return null
125+ }
126+ }
127+
85128/**
86129 * 检查图片是否满足最小宽度要求
87130 * @param {string } imageUrl - 图片 URL
@@ -117,7 +160,7 @@ export async function fetchLinkMetadata(url) {
117160 }
118161
119162 // 检测是否为视频 URL
120- const { isVideo } = detectVideoUrl ( url )
163+ const { isVideo, platform } = detectVideoUrl ( url )
121164
122165 // Bilibili 视频优先使用官方 API
123166 if ( url . includes ( 'bilibili.com' ) && url . match ( / B V [ \w ] + / ) ) {
@@ -127,6 +170,15 @@ export async function fetchLinkMetadata(url) {
127170 }
128171 }
129172
173+ // 视频平台:先走 noembed (oEmbed) — 比 Microlink 准很多 (拿到精确标题 + 高清缩略图)
174+ // 参考 absorber-youtube/helpers.py 的 fetch_video_metadata 思路 (yt-dlp 的浏览器替代)
175+ // 覆盖 YouTube / Vimeo / TikTok / 抖音 / Twitter / SoundCloud 等 noembed 支持的平台
176+ if ( isVideo && platform && platform !== 'bilibili' ) {
177+ const oembed = await fetchOembedMetadata ( url )
178+ if ( oembed ) return oembed
179+ // 失败继续往下兜底 Microlink
180+ }
181+
130182 // 使用 Microlink API 获取元数据
131183 try {
132184 const response = await fetch ( `${ MICROLINK_API } ?url=${ encodeURIComponent ( url ) } ` )
0 commit comments