148 lines
4.2 KiB
JavaScript
148 lines
4.2 KiB
JavaScript
"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 };
|