"use strict"; /** * 从混杂文本中提取抖音分享内容(链接、文案、话题)。 * 自 zhenqianba DouyinContentParser 移植,CommonJS 导出供 require。 */ const DIRECT_VIDEO_PATTERN = /https?:\/\/[^\s"'<>]+?\.(mp4|mov|m4v|webm|avi|mkv)(\?[^\s"'<>]*)?/i; class DouyinContentParser { /** * @param {string} input * @returns {{ success: true, content: object } | { success: false, error: string }} */ static parseContent(input) { try { const raw = String(input || "").trim(); if (!raw) { return { success: false, error: "输入内容为空" }; } const videoUrl = this.extractVideoUrl(raw); if (!videoUrl) { return { success: false, error: "未找到有效的抖音视频链接" }; } const description = this.extractDescription(raw, videoUrl); const hashtags = this.extractHashtags(raw); return { success: true, content: { videoUrl, description, hashtags, rawText: raw, }, }; } catch (error) { return { success: false, error: `解析失败: ${error instanceof Error ? error.message : String(error)}`, }; } } static extractVideoUrl(text) { const directVideoMatch = text.match(DIRECT_VIDEO_PATTERN); if (directVideoMatch && directVideoMatch[0]) { return directVideoMatch[0]; } const patterns = [ /https:\/\/v\.douyin\.com\/[\w_\-@]+\/?/i, /https:\/\/(?:www\.)?douyin\.com\/(?:video|modal)\/\d+/i, /https:\/\/iesdouyin\.com\/(?:share\/video|video)\/\d+/i, /v\.douyin\.com\/[\w_\-@]+/i, /douyin\.com\/(?:video|modal)\/\d+/i, ]; for (const pattern of patterns) { const match = text.match(pattern); if (match) { let url = match[0]; if (!url.startsWith("http")) { url = "https://" + url; } return url; } } return null; } static isDirectVideoUrl(url) { return DIRECT_VIDEO_PATTERN.test(String(url || "").trim()); } static extractDescription(text, videoUrl) { const urlIndex = text.indexOf(videoUrl); let descriptionText = ""; if (urlIndex > 0) { descriptionText = text.substring(0, urlIndex).trim(); } else { const descriptionMatch = text.match(/^([^h]*?)(?:https?:\/\/|$)/); if (descriptionMatch && descriptionMatch[1]) { descriptionText = descriptionMatch[1].trim(); } } return this.cleanDescription(descriptionText); } static cleanDescription(text) { if (!text) return ""; let cleaned = text.replace(/^[\d\s./]+/, "").trim(); const startMatch = cleaned.match(/^[^\u4e00-\u9fff\w\s]*(.*)$/); if (startMatch && startMatch[1]) { const potential = startMatch[1].trim(); if (potential.length > 0) { cleaned = potential; } } if (/^[\W_]+$/.test(cleaned)) { return ""; } return cleaned; } static extractHashtags(text) { const hashtagPattern = /#[\w\u4e00-\u9fff_]+/g; const matches = text.match(hashtagPattern); if (!matches) return []; return Array.from(new Set(matches)); } static extractVideoId(videoUrl) { const shortMatch = videoUrl.match(/v\.douyin\.com\/([\w_\-@]+)/i); if (shortMatch) return shortMatch[1]; const standardMatch = videoUrl.match(/\/(?:video|modal)\/(\d+)/i); if (standardMatch) return standardMatch[1]; return null; } static generateTitleFromDescription(description, maxLength = 30) { if (!description) { return "分享精彩内容"; } const sentenceEnd = description.match(/[。!?\n]/); let title = ""; if (sentenceEnd) { title = description.substring(0, sentenceEnd.index).trim(); } else { title = description; } if (title.length > maxLength) { title = title.substring(0, maxLength) + "..."; } return title || "精彩分享"; } static quickExtractUrl(text) { const r = this.parseContent(text); return r.success && r.content ? r.content.videoUrl : null; } static quickExtractDescription(text) { const r = this.parseContent(text); return r.success && r.content ? r.content.description : null; } } module.exports = { DouyinContentParser, DIRECT_VIDEO_PATTERN };