Add Facebook #17
@@ -1,6 +1,12 @@
|
|||||||
import { ResolveError, oneSegment, type Media, type Post } from '../types.ts';
|
import { ResolveError, oneSegment, type Media, type Post } from '../types.ts';
|
||||||
import { readOpenGraph } from '../browser/capture.ts';
|
import { readOpenGraph } from '../browser/capture.ts';
|
||||||
import { findCaption, findMetaMedia, mediaFromMetaNode, metaHeaders } from './meta-media.ts';
|
import {
|
||||||
|
findCaption,
|
||||||
|
findMetaMedia,
|
||||||
|
mediaFromMetaNode,
|
||||||
|
mediaFromOpenGraph,
|
||||||
|
metaHeaders,
|
||||||
|
} from './meta-media.ts';
|
||||||
import { extractJsonObjectAfterKey } from './scan.ts';
|
import { extractJsonObjectAfterKey } from './scan.ts';
|
||||||
import { hostMatcher } from './url.ts';
|
import { hostMatcher } from './url.ts';
|
||||||
import type { PlatformSpec, ResolveContext } from './types.ts';
|
import type { PlatformSpec, ResolveContext } from './types.ts';
|
||||||
@@ -202,22 +208,8 @@ function fromPayloads(texts: string[]): Scraped | undefined {
|
|||||||
/** Absolute floor: whatever the page offers a link preview. */
|
/** Absolute floor: whatever the page offers a link preview. */
|
||||||
async function fromOpenGraph(ctx: ResolveContext): Promise<Scraped> {
|
async function fromOpenGraph(ctx: ResolveContext): Promise<Scraped> {
|
||||||
const og = await readOpenGraph(ctx.page);
|
const og = await readOpenGraph(ctx.page);
|
||||||
const fetchHeaders = metaHeaders(REFERER);
|
|
||||||
const media: Media[] = [];
|
|
||||||
|
|
||||||
if (og['og:video']) {
|
|
||||||
media.push({
|
|
||||||
kind: 'video',
|
|
||||||
url: og['og:video'],
|
|
||||||
fetchHeaders,
|
|
||||||
...(og['og:image'] ? { poster: { url: og['og:image'], fetchHeaders } } : {}),
|
|
||||||
});
|
|
||||||
} else if (og['og:image']) {
|
|
||||||
media.push({ kind: 'image', url: og['og:image'], fetchHeaders });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
media,
|
media: mediaFromOpenGraph(og, REFERER),
|
||||||
...(og['og:description'] ? { text: og['og:description'] } : {}),
|
...(og['og:description'] ? { text: og['og:description'] } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { Media } from '../types.ts';
|
import type { Media } from '../types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instagram and Threads are the same product underneath and serve the same
|
* Instagram, Threads and Facebook are the same product underneath. The first
|
||||||
* media schema, so both adapters normalize through here.
|
* two serve the same media schema, so both normalize through here; Facebook's
|
||||||
|
* is its own, but the headers its CDN wants and the link preview it falls
|
||||||
|
* back to are shared with them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Candidate = { url?: string; width?: number; height?: number };
|
type Candidate = { url?: string; width?: number; height?: number };
|
||||||
@@ -66,6 +68,27 @@ export function mediaFromMetaNode(node: MetaMediaNode, referer: string): Media[]
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link preview as media: the floor on all three, and all that is left
|
||||||
|
* when a post is refused to a logged-out reader.
|
||||||
|
*/
|
||||||
|
export function mediaFromOpenGraph(og: Record<string, string>, referer: string): Media[] {
|
||||||
|
const fetchHeaders = metaHeaders(referer);
|
||||||
|
|
||||||
|
if (og['og:video']) {
|
||||||
|
return [{
|
||||||
|
kind: 'video',
|
||||||
|
url: og['og:video'],
|
||||||
|
fetchHeaders,
|
||||||
|
...(og['og:image'] ? { poster: { url: og['og:image'], fetchHeaders } } : {}),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
if (og['og:image']) {
|
||||||
|
return [{ kind: 'image', url: og['og:image'], fetchHeaders }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
function nonEmptyArray(value: unknown): boolean {
|
function nonEmptyArray(value: unknown): boolean {
|
||||||
return Array.isArray(value) && value.length > 0;
|
return Array.isArray(value) && value.length > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { ResolveError, type Media, type Post, type Segment } from '../types.ts';
|
import { ResolveError, type Post, type Segment } from '../types.ts';
|
||||||
import { readOpenGraph } from '../browser/capture.ts';
|
import { readOpenGraph } from '../browser/capture.ts';
|
||||||
import { findCaption, findMetaMedia, mediaFromMetaNode, metaHeaders, type MetaMediaNode } from './meta-media.ts';
|
import {
|
||||||
|
findCaption,
|
||||||
|
findMetaMedia,
|
||||||
|
mediaFromMetaNode,
|
||||||
|
mediaFromOpenGraph,
|
||||||
|
type MetaMediaNode,
|
||||||
|
} from './meta-media.ts';
|
||||||
import { hostMatcher } from './url.ts';
|
import { hostMatcher } from './url.ts';
|
||||||
import type { PlatformSpec, ResolveContext } from './types.ts';
|
import type { PlatformSpec, ResolveContext } from './types.ts';
|
||||||
|
|
||||||
@@ -129,19 +135,7 @@ function segmentOf(post: ThreadsPost, isAnchor: boolean): Segment {
|
|||||||
/** The floor, when the structured payload is not there to be read. */
|
/** The floor, when the structured payload is not there to be read. */
|
||||||
async function fromOpenGraph(ctx: ResolveContext): Promise<Segment | undefined> {
|
async function fromOpenGraph(ctx: ResolveContext): Promise<Segment | undefined> {
|
||||||
const og = await readOpenGraph(ctx.page);
|
const og = await readOpenGraph(ctx.page);
|
||||||
const fetchHeaders = metaHeaders(REFERER);
|
const media = mediaFromOpenGraph(og, REFERER);
|
||||||
const media: Media[] = [];
|
|
||||||
|
|
||||||
if (og['og:video']) {
|
|
||||||
media.push({
|
|
||||||
kind: 'video',
|
|
||||||
url: og['og:video'],
|
|
||||||
fetchHeaders,
|
|
||||||
...(og['og:image'] ? { poster: { url: og['og:image'], fetchHeaders } } : {}),
|
|
||||||
});
|
|
||||||
} else if (og['og:image']) {
|
|
||||||
media.push({ kind: 'image', url: og['og:image'], fetchHeaders });
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = og['og:description'];
|
const text = og['og:description'];
|
||||||
if (media.length === 0 && !text) return undefined;
|
if (media.length === 0 && !text) return undefined;
|
||||||
|
|||||||
+25
-1
@@ -1,6 +1,11 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { findCaption, findMetaMedia, mediaFromMetaNode } from '../src/platforms/meta-media.ts';
|
import {
|
||||||
|
findCaption,
|
||||||
|
findMetaMedia,
|
||||||
|
mediaFromMetaNode,
|
||||||
|
mediaFromOpenGraph,
|
||||||
|
} from '../src/platforms/meta-media.ts';
|
||||||
import { fixture } from './helpers.ts';
|
import { fixture } from './helpers.ts';
|
||||||
|
|
||||||
const REFERER = 'https://www.threads.com/';
|
const REFERER = 'https://www.threads.com/';
|
||||||
@@ -68,3 +73,22 @@ test('the caption is found the same way, and an empty one is not a caption', ()
|
|||||||
assert.equal(findCaption({ x: { caption: { text: '' } } }), undefined);
|
assert.equal(findCaption({ x: { caption: { text: '' } } }), undefined);
|
||||||
assert.equal(findCaption({ x: { caption: null } }), undefined);
|
assert.equal(findCaption({ x: { caption: null } }), undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the link preview stands in for the post when there is nothing better', () => {
|
||||||
|
const image = mediaFromOpenGraph({ 'og:image': 'https://image.example/a.jpg' }, REFERER);
|
||||||
|
assert.deepEqual(image.map((item) => [item.kind, item.url]), [['image', 'https://image.example/a.jpg']]);
|
||||||
|
assert.equal(image[0]?.fetchHeaders?.['Referer'], REFERER);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a preview naming a video is a video, and its image is the poster', () => {
|
||||||
|
const media = mediaFromOpenGraph(
|
||||||
|
{ 'og:video': 'https://video.example/a.mp4', 'og:image': 'https://image.example/a.jpg' },
|
||||||
|
REFERER,
|
||||||
|
);
|
||||||
|
assert.equal(media[0]?.kind, 'video');
|
||||||
|
assert.equal(media[0]?.kind === 'video' ? media[0].poster?.url : undefined, 'https://image.example/a.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a page with no preview at all yields no media rather than an empty asset', () => {
|
||||||
|
assert.deepEqual(mediaFromOpenGraph({ 'og:title': 'just a title' }, REFERER), []);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user