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 };
|