From df948ce9e6511069f30fd73a70fa546b28ef2442 Mon Sep 17 00:00:00 2001 From: dale-wahl <32108944+dale-wahl@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:57:32 +0000 Subject: [PATCH 1/2] chore: sync map_item for xiaohongshu from 4CAT 451220dd9d7ca1e711178185db8a95e3b193c112 --- modules/rednote.js | 172 +++++++++++++++++++++++++++------------------ 1 file changed, 102 insertions(+), 70 deletions(-) diff --git a/modules/rednote.js b/modules/rednote.js index e42d04f..239f536 100644 --- a/modules/rednote.js +++ b/modules/rednote.js @@ -107,18 +107,34 @@ export function capture(response, source_platform_url, source_url) { // === auto-generated by 4cat map_item sync — BLOCK REPLACED AUTOMATICALLY === // (regenerated from datasources/xiaohongshu/search_rednote.py) +export function map_item(post) { + // Reject tile stub items + if (!('note_card' in post) && !('user' in post) && py_get(post, '_zs-origin') !== 'html' && !('note' in post)) { + const source = py_get(py_get(post, '__import_meta', {}), 'source_url', ''); + throw new MapItemException(`Xiaohongshu tile stub without post content (source: ${source || 'unknown'})`); + } + + if (py_get(post, '_zs-origin') === 'html') { + return map_item_from_html(post); + } else if ('note' in post) { + return map_item_from_json_embedded(post); + } else { + return map_item_from_json_api_explore(post); + } +} + function map_item_from_json_api_explore(post) { - const item = post.type !== 'video' ? post.note_card : post; - const item_id = post.id ?? post.note_id; + const item = py_get(post, 'type') !== 'video' ? py_get(post, 'note_card') : post; + const item_id = py_get(post, 'id', py_get(post, 'note_id')); // Images handling let images; - if (item.image_list) { + if (item && item.image_list) { images = []; for (const image of item.image_list) { - if (image.url_default) { + if ('url_default' in image && image.url_default) { images.push(image.url_default); - } else if (image.info_list && image.info_list.length) { + } else if ('info_list' in image && image.info_list) { let found = false; for (const imgInfo of image.info_list) { if (imgInfo.image_scene === 'WB_DFT') { @@ -127,112 +143,128 @@ function map_item_from_json_api_explore(post) { break; } } - if (!found) { + if (!found && image.info_list.length) { images.push(image.info_list[0].url); } } } - } else if (item.cover) { + } else if (item && item.cover) { images = [item.cover.url_default]; } else { images = new MissingMappedField(""); } - const xsec_bit = post.xsec_token ? `?xsec_token=${post.xsec_token}` : ""; - const video_url = item.video?.media ? item.video.media.stream.h264[0].master_url : new MissingMappedField(""); - const author = item.user.nickname ?? item.user.nick_name; - const timestamp = item.time ?? null; - const timestampStr = timestamp ? formatUtcTimestamp(timestamp / 1000) : new MissingMappedField(""); - const hashtags = item.desc ? [...item.desc.matchAll(/#([^\s!@#$%^&*()_+{}:"|<>?\[\];',.\/`~]+)/g)].map(m => m[1]).join(",") : new MissingMappedField(""); - const body = item.desc ?? new MissingMappedField(""); - const image_urls = Array.isArray(images) ? images.join(",") : images; - const likes = item.interact_info?.liked_count ?? null; - const unix_ts = timestamp ? Math.floor(timestamp / 1000) : new MissingMappedField(""); + const xsec_bit = py_get(post, 'xsec_token') ? `?xsec_token=${post.xsec_token}` : ""; + let video_url; + if (item && item.video && item.video.media) { + video_url = getVideoUrl(item.video.media.stream || {}); + } else { + video_url = new MissingMappedField(""); + } + + const author = (item.user.nickname !== undefined) ? item.user.nickname : item.user.nick_name; + const timestamp = py_get(item, 'time'); + + const hashtags = ('desc' in item) + ? [...item.desc.matchAll(/#([^\s!@#$%^&*()_+{}:"|<>?\[\];',.\/`~]+)/g)].map(m => m[1]).join(',') + : new MissingMappedField(""); + + const body = ('desc' in item) ? item.desc : new MissingMappedField(""); + + const collected_from_url = normalize_url_encoding(py_get(py_get(post, '__import_meta', {}), 'source_platform_url', '')); return new MappedItem({ - collected_from_url: normalize_url_encoding(post.__import_meta?.source_platform_url ?? ""), + collected_from_url, id: item_id, thread_id: item_id, url: `https://www.xiaohongshu.com/explore/${post.id}${xsec_bit}`, - title: item.display_title ?? "", - body: body, - hashtags: hashtags, - timestamp: timestampStr, - author: author, + title: py_get(item, 'display_title', ''), + body, + hashtags, + timestamp: timestamp ? formatUtcTimestamp(timestamp / 1000) : new MissingMappedField("") , + author, author_avatar_url: item.user.avatar, - image_urls: image_urls, - video_url: video_url, - likes: likes, - unix_timestamp: unix_ts, + image_urls: Array.isArray(images) ? images.join(',') : images, + video_url, + likes: item.interact_info.liked_count, + unix_timestamp: timestamp ? Math.floor(timestamp / 1000) : new MissingMappedField("") }); } +function getVideoUrl(streams) { + const h264 = ['h264', 'EF4']; + const formats = [...h264, ...Object.keys(streams).filter(k => !h264.includes(k))]; + for (const name of formats) { + const list = streams[name] || []; + for (const stream of list) { + if (stream.master_url) { + return stream.master_url; + } + } + } + return new MissingMappedField(""); +} + function map_item_from_json_embedded(item) { const note = item.note; - const image = note.imageList?.[0]?.urlDefault ?? new MissingMappedField(""); + const image = note.imageList[0].urlDefault; const xsec_bit = `?xsec_token=${note.xsecToken}`; - const timestamp = note.time ?? null; - const timestampStr = timestamp ? formatUtcTimestamp(timestamp / 1000) : new MissingMappedField(""); - const hashtags = note.desc ? [...note.desc.matchAll(/#([^\s!@#$%^&*()_+{}:"|<>?\[\];',.\/`~]+)/g)].map(m => m[1]).join(",") : new MissingMappedField(""); - const body = note.desc ?? new MissingMappedField(""); - const author = note.user.nickname ?? note.user.nick_name; - const likes = note.interactInfo?.likedCount ?? - note.interact_info?.liked_count ?? - note.likes ?? - new MissingMappedField(""); - const unix_ts = timestamp ? Math.floor(timestamp / 1000) : new MissingMappedField(""); + const timestamp = py_get(note, 'time'); + + let likes; + if ('interactInfo' in note) { + likes = note.interactInfo.likedCount; + } else if ('interact_info' in note) { + likes = note.interact_info.liked_count; + } else if ('likes' in note) { + likes = note.likes; + } else { + likes = new MissingMappedField(""); + } + + const author = (note.user.nickname !== undefined) ? note.user.nickname : note.user.nick_name; + const hashtags = ('desc' in note) + ? [...note.desc.matchAll(/#([^\s!@#$%^&*()_+{}:"|<>?\[\];',.\/`~]+)/g)].map(m => m[1]).join(',') + : new MissingMappedField(""); + const body = ('desc' in note) ? note.desc : new MissingMappedField(""); + + const collected_from_url = normalize_url_encoding(py_get(py_get(item, '__import_meta', {}), 'source_platform_url', '')); return new MappedItem({ - collected_from_url: normalize_url_encoding(item.__import_meta?.source_platform_url ?? ""), + collected_from_url, id: item.id, thread_id: item.id, url: `https://www.xiaohongshu.com/explore/${item.id}${xsec_bit}`, - title: note.title ?? "", - body: body, - hashtags: hashtags, - timestamp: timestampStr, - author: author, + title: py_get(note, 'title', ''), + body, + hashtags, + timestamp: timestamp ? formatUtcTimestamp(timestamp / 1000) : new MissingMappedField("") , + author, author_avatar_url: note.user.avatar, image_url: image, - video_url: new MissingMappedField(""), - likes: likes, - unix_timestamp: unix_ts, + video_url: new MissingMappedField("") , + likes, + unix_timestamp: timestamp ? Math.floor(timestamp / 1000) : new MissingMappedField("") }); } function map_item_from_html(item) { + const collected_from_url = normalize_url_encoding(py_get(py_get(item, '__import_meta', {}), 'source_platform_url', '')); return new MappedItem({ - collected_from_url: normalize_url_encoding(item.__import_meta?.source_platform_url ?? ""), + collected_from_url, id: item.id, thread_id: item.id, url: `https://www.xiaohongshu.com${item.url}`, title: item.title, - body: new MissingMappedField(""), - hashtags: new MissingMappedField(""), - timestamp: new MissingMappedField(""), + body: new MissingMappedField("") , + hashtags: new MissingMappedField("") , + timestamp: new MissingMappedField("") , author: item.author_name, author_avatar_url: item.author_avatar_url, image_url: item.thumbnail_url, - video_url: new MissingMappedField(""), + video_url: new MissingMappedField("") , likes: item.likes, - unix_timestamp: new MissingMappedField(""), + unix_timestamp: new MissingMappedField("") }); } - -export function map_item(post) { - // Reject tile stub items – minimal thumbnail entries with no content - if (!post.note_card && !post.user && post['_zs-origin'] !== 'html' && !post.note) { - const source = post.__import_meta?.source_url ?? ""; - throw new MapItemException(`Xiaohongshu tile stub without post content (source: ${source || 'unknown'})`); - } - if (post['_zs-origin'] === 'html') { - return map_item_from_html(post); - } else { - if (post.note) { - return map_item_from_json_embedded(post); - } else { - return map_item_from_json_api_explore(post); - } - } -} // === end auto-generated === From 805a1a31bc6177f655855774398bb84f68683af7 Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 21 Oct 2026 17:08:08 +0200 Subject: [PATCH 2/2] rednote fix empty list return --- modules/rednote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rednote.js b/modules/rednote.js index cc2d072..fa878d2 100644 --- a/modules/rednote.js +++ b/modules/rednote.js @@ -131,7 +131,7 @@ function map_item_from_json_api_explore(post) { // Images handling let images; - if (item && item.image_list) { + if (item && Array.isArray(item.image_list) && item.image_list.length) { images = []; for (const image of item.image_list) { if ('url_default' in image && image.url_default) {