tail-fin-xhs 0.7.8

Xiaohongshu adapter for tail-fin: search, notes, comments, feed
Documentation
pub const JS: &str = r#"(() => {
    const loginWall = /登录后查看|请登录|登录后/.test(document.body.innerText);
    const notFound = /用户不存在|页面不见了/.test(document.body.innerText);
    if (loginWall) return { loginWall: true, notFound: false, media: [] };
    if (notFound) return { loginWall: false, notFound: true, media: [] };

    const mediaSet = new Set();
    const media = [];

    function addMedia(type, url) {
        if (!url || mediaSet.has(url)) return;
        mediaSet.add(url);
        media.push({ type, url });
    }

    // Method 1: __INITIAL_STATE__.note.noteDetailMap
    const state = window.__INITIAL_STATE__;
    if (state && state.note && state.note.noteDetailMap) {
        const map = state.note.noteDetailMap;
        const keys = Object.keys(map);
        for (const key of keys) {
            const detail = map[key];
            const note = (detail && detail.note) ? detail.note : detail;
            if (!note) continue;

            // Video extraction
            if (note.video) {
                const v = note.video;
                let videoUrl = null;
                if (v.media && v.media.stream) {
                    const stream = v.media.stream;
                    const h264 = stream.h264 || stream.H264;
                    if (Array.isArray(h264) && h264.length > 0) {
                        videoUrl = h264[0].masterUrl || h264[0].url || null;
                    }
                }
                if (!videoUrl && v.media && v.media.originVideoKey) {
                    videoUrl = 'https://sns-video-bd.xhscdn.com/' + v.media.originVideoKey;
                }
                if (!videoUrl && v.url) {
                    videoUrl = v.url;
                }
                if (videoUrl) addMedia('video', videoUrl);
            }

            // Image extraction
            if (note.imageList && Array.isArray(note.imageList)) {
                for (const img of note.imageList) {
                    let imgUrl = null;
                    if (img.infoList && Array.isArray(img.infoList) && img.infoList.length > 0) {
                        // Pick best resolution (last entry tends to be highest)
                        const best = img.infoList[img.infoList.length - 1];
                        imgUrl = best.url || null;
                    }
                    if (!imgUrl) imgUrl = img.urlDefault || img.url || null;
                    if (imgUrl) addMedia('image', imgUrl);
                }
            }
        }
    }

    // Method 2: Inline <script> tags regex for video URLs
    if (media.filter(m => m.type === 'video').length === 0) {
        const scripts = document.querySelectorAll('script:not([src])');
        const videoRe = /(https?:\/\/(?:sns-video[^"' ]*\.mp4|[^"' ]*xhscdn[^"' ]*\.mp4))/g;
        for (const script of scripts) {
            let match;
            while ((match = videoRe.exec(script.textContent)) !== null) {
                addMedia('video', match[1]);
            }
        }
    }

    // Method 3: DOM <video> elements
    if (media.filter(m => m.type === 'video').length === 0) {
        const videos = document.querySelectorAll('video[src], video source[src]');
        for (const el of videos) {
            const src = el.src || el.getAttribute('src');
            if (src && !src.startsWith('blob:')) addMedia('video', src);
        }
    }

    // Method 4: DOM <img> matching XHS CDN patterns
    if (media.filter(m => m.type === 'image').length === 0) {
        const imgs = document.querySelectorAll('img[src]');
        for (const img of imgs) {
            const src = img.src || img.getAttribute('src');
            if (src && (src.includes('ci.xiaohongshu.com') || src.includes('xhscdn'))) {
                addMedia('image', src);
            }
        }
    }

    return { loginWall: false, notFound: false, media };
})()"#;