tail-fin-xhs 0.7.8

Xiaohongshu adapter for tail-fin: search, notes, comments, feed
Documentation
pub const JS: &str = r#"(() => {
    const loginWallPattern = /登录后查看|请登录|登录后/;
    const notFoundPattern = /页面不见了|笔记不存在|无法浏览|内容已删除/;
    const bodyText = document.body ? document.body.innerText : '';

    const loginWall = loginWallPattern.test(bodyText);
    const notFound = notFoundPattern.test(bodyText);

    if (loginWall || notFound) {
        return { loginWall, notFound };
    }

    try {
        const state = window.__INITIAL_STATE__;
        if (!state || !state.note || !state.note.noteDetailMap) {
            return { loginWall, notFound, error: "noteDetailMap not found in __INITIAL_STATE__" };
        }

        const noteDetailMap = state.note.noteDetailMap;
        const keys = Object.keys(noteDetailMap);
        if (keys.length === 0) {
            return { loginWall, notFound, error: "noteDetailMap is empty" };
        }

        const entry = noteDetailMap[keys[0]];
        const note = entry && (entry.note || entry);

        if (!note) {
            return { loginWall, notFound, error: "note entry is null or undefined" };
        }

        // Basic fields
        const id = note.noteId || note.id || '';
        const title = note.title || '';
        const content = note.desc || note.content || '';

        // Author
        const user = note.user || {};
        const author = user.nickname || user.name || '';
        const authorId = user.userId || user.id || '';

        // Interact info
        const interactInfo = note.interactInfo || {};
        function parseCount(val) {
            if (!val) return 0;
            if (typeof val === 'number') return val;
            const s = String(val).replace(/[^\d]/g, '');
            return s ? parseInt(s, 10) : 0;
        }
        const likes = parseCount(interactInfo.likedCount);
        const collects = parseCount(interactInfo.collectedCount);
        const comments = parseCount(interactInfo.commentCount);
        const shares = parseCount(interactInfo.shareCount);

        // Tags
        const tagList = note.tagList || [];
        const tags = tagList.map(t => t.name || t.text || t).filter(t => typeof t === 'string' && t.length > 0);

        // Images — prefer highest resolution from infoList
        const imageList = note.imageList || [];
        const images = imageList.map(img => {
            const infoList = img.infoList || [];
            if (infoList.length > 0) {
                // Sort by width descending, take the best
                const sorted = infoList.slice().sort((a, b) => (b.width || 0) - (a.width || 0));
                return sorted[0].url || '';
            }
            return img.url || img.urlDefault || '';
        }).filter(u => u.length > 0);

        // Video URL — check note.video with stream/h264 priority
        let video = null;
        const CDN_PREFIX = 'https://sns-video-bd.xhscdn.com/';
        const videoData = note.video || {};
        if (videoData) {
            // Try stream.h264 first
            const stream = videoData.stream || {};
            const h264List = stream.h264 || [];
            if (h264List.length > 0) {
                video = h264List[0].masterUrl || h264List[0].backupUrls && h264List[0].backupUrls[0] || null;
            }
            // Fallback: try video.url
            if (!video && videoData.url) {
                video = videoData.url;
            }
            // Fallback: originVideoKey with CDN prefix
            if (!video && videoData.originVideoKey) {
                video = CDN_PREFIX + videoData.originVideoKey;
            }
        }

        // Note type
        const noteType = note.type || note.noteType || 'normal';

        // Published at — decode from MongoDB ObjectID (first 8 hex chars = unix timestamp)
        let publishedAt = '';
        if (id && id.length >= 8) {
            try {
                const ts = parseInt(id.substring(0, 8), 16) * 1000;
                if (!isNaN(ts)) publishedAt = new Date(ts).toISOString().split('T')[0];
            } catch (e) {}
        }

        return {
            loginWall,
            notFound,
            note: {
                id,
                title,
                content,
                author,
                authorId,
                likes,
                collects,
                comments,
                shares,
                tags,
                images,
                video,
                publishedAt,
                noteType,
            }
        };
    } catch (e) {
        return { loginWall, notFound, error: String(e) };
    }
})()
"#;