#!/usr/bin/env node const { DouyinApi } = require('../claude-code/claude-code-voc-intelligence/mcp/src/providers/douyin-api'); function parseArgs(argv) { const args = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith('--')) continue; const eq = token.indexOf('='); if (eq > 0) { args[token.slice(2, eq)] = token.slice(eq + 1); continue; } const key = token.slice(2); const next = argv[i + 1]; if (next && !next.startsWith('--')) { args[key] = next; i += 1; } else { args[key] = true; } } return args; } function decodeMaybeBase64Url(value) { const text = String(value || '').trim(); if (!text) return ''; if (/^https?:\/\//i.test(text)) return text; if (!/^[A-Za-z0-9+/=_-]+$/.test(text) || text.length < 24) return ''; try { const decoded = Buffer.from(text.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'); return /^https?:\/\//i.test(decoded) ? decoded : ''; } catch { return ''; } } function collectUrls(node, path = [], out = []) { if (!node) return out; if (Array.isArray(node)) { node.forEach((item, index) => collectUrls(item, [...path, String(index)], out)); return out; } if (typeof node !== 'object') { if (typeof node === 'string') { const url = decodeMaybeBase64Url(node); if (url) out.push({ url, path: path.join('.') }); } return out; } Object.entries(node).forEach(([key, value]) => collectUrls(value, [...path, key], out)); return out; } function isMediaUrl(url) { const lower = String(url || '').toLowerCase(); return lower.includes('douyinvod.com') || lower.includes('/aweme/v1/play') || lower.includes('mime_type=video') || /\.(mp4|m4a|mp3|aac|wav|webm)(?:\?|$)/i.test(lower); } function findAweme(node, awemeId, depth = 0) { if (!node || depth > 8) return null; if (Array.isArray(node)) { for (const item of node) { const found = findAweme(item, awemeId, depth + 1); if (found) return found; } return null; } if (typeof node !== 'object') return null; const id = String(node.aweme_id || node.id || ''); if (id === String(awemeId)) return node; for (const value of Object.values(node)) { const found = findAweme(value, awemeId, depth + 1); if (found) return found; } return null; } async function main() { const args = parseArgs(process.argv.slice(2)); const token = process.env.VOC_TOKEN || process.env.VOC_DOUYIN_TOKEN || process.env.DOUYIN_TOKEN; if (!token) throw new Error('VOC_TOKEN is required in environment'); if (!args.keyword) throw new Error('--keyword is required'); const api = new DouyinApi({ token }); const response = await api.searchVideos({ keyword: args.keyword, cursor: Number(args.cursor || 0), sortType: args.sortType || '1', publishTime: args.publishTime || '180', filterDuration: args.filterDuration || '0', contentType: args.contentType || '1', }); const target = args.awemeId ? findAweme(response.data, args.awemeId) : response.data; const urls = collectUrls(target || response.data) .filter(item => isMediaUrl(item.url)) .filter((item, index, list) => list.findIndex(other => other.url === item.url) === index) .slice(0, 20); console.log(JSON.stringify({ status: 'ok', foundTarget: Boolean(target), mediaUrlCount: urls.length, mediaCandidates: urls.map(item => ({ path: item.path, urlPrefix: item.url.slice(0, 180), })), }, null, 2)); } main().catch(error => { console.error(JSON.stringify({ status: 'error', message: error.message }, null, 2)); process.exit(1); });