This commit is contained in:
fengchuanhn@gmail.com
2026-05-17 12:54:13 +08:00
parent 1b8ca10ec9
commit 3db327d93b
38 changed files with 2820 additions and 14 deletions

View File

@@ -0,0 +1,147 @@
"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 };