recipe-manager/test_facebook_scraper.js

135 lines
5.6 KiB
JavaScript

const assert = require('assert');
// 1. URL detection helper
function isFacebookUrl(url) {
try {
const parsed = new URL(url);
const host = parsed.hostname.toLowerCase();
const isFb = host.includes('facebook.com') || host.includes('fb.watch') || host.includes('fb.com');
if (!isFb) return false;
return host.includes('fb.watch') ||
parsed.pathname.includes('/reel') ||
parsed.pathname.includes('/watch') ||
parsed.pathname.includes('/videos') ||
parsed.pathname.includes('/share/');
} catch (e) {
return false;
}
}
function cleanSocialCaption(raw) {
if (!raw) return '';
let text = raw.replace(/(?:\s*#[^\s#]+)+\s*$/g, '').trim();
text = text.replace(/[\uFE00-\uFE0F\uD83C-\uDBFF\uDC00-\uDFFF\u2600-\u26FF\u2700-\u27BF]+$/, '').trim();
return text;
}
function cleanFacebookCaption(raw) {
if (!raw) return '';
let text = raw.trim();
const quotedMatch = text.match(/(?:on Facebook:\s*["“]|:\s*["“])([\s\S]*?)["”]?$/i);
if (quotedMatch && quotedMatch[1]) {
text = quotedMatch[1];
} else {
const colonQuote = text.indexOf(': "');
if (colonQuote !== -1) {
text = text.slice(colonQuote + 3).replace(/["”]$/, '');
}
}
text = text.replace(/\s*\|\s*By\s+[^|]+\|\s*Facebook.*$/i, '');
text = text.replace(/\s*\|\s*Facebook.*$/i, '');
return cleanSocialCaption(text);
}
function isCaptionTruncated(text) {
if (!text || typeof text !== 'string') return false;
const trimmed = text.trim();
if (/(?:\.{3,}|…)\s*(?:See\s+more)?$/i.test(trimmed)) return true;
if (/\bSee\s+more$/i.test(trimmed)) return true;
if (trimmed.length >= 180 && trimmed.length <= 320) {
const endsWithPunctuation = /[.!?\n\r"']$/.test(trimmed);
if (!endsWithPunctuation) return true;
}
return false;
}
function extractFullFacebookMessageFromHtml(html) {
if (!html || typeof html !== 'string') return '';
let longestText = '';
const isBoilerplate = (str) => /log in|watch popular videos|sign up for facebook|connect with friends/i.test(str);
const messageRegex = /"(?:message|savable_description)"\s*:\s*\{\s*"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
let match;
while ((match = messageRegex.exec(html)) !== null) {
try {
const decoded = JSON.parse(`"${match[1]}"`);
if (decoded && decoded.length > longestText.length && !isBoilerplate(decoded)) {
longestText = decoded;
}
} catch (e) {}
}
const storyRegex = /"creation_story"\s*:\s*\{[\s\S]*?"message"\s*:\s*\{\s*"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
while ((match = storyRegex.exec(html)) !== null) {
try {
const decoded = JSON.parse(`"${match[1]}"`);
if (decoded && decoded.length > longestText.length && !isBoilerplate(decoded)) {
longestText = decoded;
}
} catch (e) {}
}
const ldJsonRegex = /<script\s+type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
while ((match = ldJsonRegex.exec(html)) !== null) {
try {
const data = JSON.parse(match[1]);
const body = data.articleBody || data.description || '';
if (body && body.length > longestText.length && !isBoilerplate(body)) {
longestText = body;
}
} catch (e) {}
}
return cleanFacebookCaption(longestText);
}
console.log('Testing isFacebookUrl...');
assert.strictEqual(isFacebookUrl('https://www.facebook.com/reel/123456789'), true);
assert.strictEqual(isFacebookUrl('https://fb.watch/abcdef123/'), true);
assert.strictEqual(isFacebookUrl('https://m.facebook.com/reel/987654321/'), true);
assert.strictEqual(isFacebookUrl('https://www.facebook.com/watch/?v=11223344'), true);
assert.strictEqual(isFacebookUrl('https://www.facebook.com/share/r/abc123xyz/'), true);
assert.strictEqual(isFacebookUrl('https://www.instagram.com/reel/xyz123'), false);
console.log('✔ isFacebookUrl tests passed!');
console.log('\nTesting isCaptionTruncated...');
assert.strictEqual(isCaptionTruncated('1 cup flour, 2 eggs, 1 tbsp butter...'), true);
assert.strictEqual(isCaptionTruncated('1 cup flour, 2 eggs, 1 tbsp butter…'), true);
assert.strictEqual(isCaptionTruncated('1 cup flour, 2 eggs, 1 tbsp butter... See more'), true);
assert.strictEqual(isCaptionTruncated('1 cup flour, 2 eggs, 1 tbsp butter… See More'), true);
assert.strictEqual(isCaptionTruncated('This is a truncated facebook description that hits around two hundred characters and stops abruptly in the middle of a sentence without any final punctuation mark like a period so it indicates cut off'), true);
assert.strictEqual(isCaptionTruncated('1 cup flour, 2 eggs. Mix thoroughly and bake for 25 minutes at 350F.'), false);
console.log('✔ isCaptionTruncated tests passed!');
console.log('\nTesting extractFullFacebookMessageFromHtml...');
const sampleHtml = `
<html>
<head>
<meta property="og:description" content="Chef Gordon: Crispy Garlic Butter Steak Bites. 1 lb sirloin steak, 2 tbsp butter...">
</head>
<body>
<script type="application/json" data-sjs>
{"require":[["RelayPrefetchedStreamCache","next",[],[{"message":{"text":"Crispy Garlic Butter Steak Bites.\\n\\nIngredients:\\n- 1 lb sirloin steak\\n- 2 tbsp butter\\n- 4 cloves garlic, minced\\n\\nDirections:\\n1. Sear steak in hot pan for 3 mins.\\n2. Add butter and garlic.\\n3. Serve warm!"}}]]]}
</script>
</body>
</html>
`;
const extracted = extractFullFacebookMessageFromHtml(sampleHtml);
assert.ok(extracted.includes('4 cloves garlic, minced'));
assert.ok(extracted.includes('Directions:'));
assert.strictEqual(isCaptionTruncated(extracted), false);
console.log('✔ extractFullFacebookMessageFromHtml successfully retrieved complete message!');
console.log('\nAll Facebook Reel scraper & truncation tests passed successfully!');