@sockethub/platform-metadata 1.0.1-beta.3 → 1.0.1-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/index.js +129 -18
- package/dist/index.js.map +4 -4
- package/dist/resolvers.d.ts +63 -10
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -98,6 +98,11 @@ The platform extracts the following metadata when available:
|
|
|
98
98
|
flattened to one string, or the virtual location URL), `url`, and `status`
|
|
99
99
|
(`scheduled`, `cancelled`, `postponed`, `rescheduled`). Only present when
|
|
100
100
|
both a name and a start date are declared.
|
|
101
|
+
* **Description fallback**: pages with no meta description use a JSON-LD
|
|
102
|
+
`description`, else a `headline` not already in the title (Wikipedia's
|
|
103
|
+
short description, e.g. "1979 studio album by Brian Eno").
|
|
104
|
+
* **Non-HTML links** (PDFs and other files) return a page titled with the
|
|
105
|
+
file name instead of an error.
|
|
101
106
|
|
|
102
107
|
## Site-Specific Handling
|
|
103
108
|
|
|
@@ -108,7 +113,9 @@ so those URLs are resolved through preview services built for this purpose:
|
|
|
108
113
|
are fetched from the [FxTwitter API](https://github.com/FxEmbed/FxEmbed)
|
|
109
114
|
instead of scraped — X serves only a generic site banner to scrapers. The
|
|
110
115
|
response carries the post's text, its actual photo (or video + poster
|
|
111
|
-
thumbnail), and the canonical `x.com` URL.
|
|
116
|
+
thumbnail), and the canonical `x.com` URL. Posts without their own media
|
|
117
|
+
use the quoted post's media, then the link card image, then the author's
|
|
118
|
+
avatar. If the API is unavailable or the
|
|
112
119
|
post can't be resolved, the platform falls back to the regular scrape
|
|
113
120
|
(which still yields the post text).
|
|
114
121
|
* **Reddit post URLs** use `old.reddit.com`'s structured JSON response, avoiding
|
|
@@ -124,6 +131,13 @@ so those URLs are resolved through preview services built for this purpose:
|
|
|
124
131
|
datacenter address. Full media still requires authentication. Facebook
|
|
125
132
|
publishes no site name, so `actor.name` and `object.name` fall back to
|
|
126
133
|
`Facebook`.
|
|
134
|
+
* **Amazon product URLs** (storefronts such as `amazon.de` and `amazon.com`,
|
|
135
|
+
plus `amzn.to`/`a.co` short links) are scraped with WhatsApp's link-preview
|
|
136
|
+
user agent. Amazon answers other crawlers with a captcha or a generic
|
|
137
|
+
"Amazon" share card instead of the product's title and image. Mobile-app
|
|
138
|
+
links (`/gp/aw/d/<ASIN>`) are scraped as `/dp/<ASIN>`, which is the only
|
|
139
|
+
form that gets the product card. When Amazon serves its full desktop page
|
|
140
|
+
instead, the image comes from the page's main product image.
|
|
127
141
|
* **Facebook engagement prefixes**: video titles and image alt text often
|
|
128
142
|
start with a localized view or reaction count, such as
|
|
129
143
|
`2.2M views · 21K reactions | …` or `347K views | …`. When the prefix is
|
package/dist/index.js
CHANGED
|
@@ -70777,7 +70777,7 @@ var import_undici3 = __toESM(require_undici(), 1);
|
|
|
70777
70777
|
// package.json
|
|
70778
70778
|
var package_default = {
|
|
70779
70779
|
name: "@sockethub/platform-metadata",
|
|
70780
|
-
version: "1.0.1-beta.
|
|
70780
|
+
version: "1.0.1-beta.5",
|
|
70781
70781
|
description: "Fetch metadata from a given URL",
|
|
70782
70782
|
author: "Nick Jennings <nick@silverbucket.net>",
|
|
70783
70783
|
license: "LGPL-3.0+",
|
|
@@ -70819,17 +70819,17 @@ var package_default = {
|
|
|
70819
70819
|
"clean:deps": "rm -rf node_modules"
|
|
70820
70820
|
},
|
|
70821
70821
|
dependencies: {
|
|
70822
|
-
"@sockethub/util": "^1.0.0-beta.
|
|
70822
|
+
"@sockethub/util": "^1.0.0-beta.5",
|
|
70823
70823
|
"open-graph-scraper": "^6.11.0",
|
|
70824
70824
|
undici: "^7.30.0"
|
|
70825
70825
|
},
|
|
70826
70826
|
devDependencies: {
|
|
70827
|
-
"@sockethub/schemas": "^3.0.0-beta.
|
|
70827
|
+
"@sockethub/schemas": "^3.0.0-beta.5"
|
|
70828
70828
|
},
|
|
70829
70829
|
peerDependencies: {
|
|
70830
|
-
"@sockethub/server": "^5.0.0-beta.
|
|
70830
|
+
"@sockethub/server": "^5.0.0-beta.5"
|
|
70831
70831
|
},
|
|
70832
|
-
gitHead: "
|
|
70832
|
+
gitHead: "cc4ffdd9714ebbd8372a88c4f2d795974e04029e",
|
|
70833
70833
|
types: "./dist/index.d.ts"
|
|
70834
70834
|
};
|
|
70835
70835
|
|
|
@@ -71074,6 +71074,68 @@ function isFacebookUrl(url) {
|
|
|
71074
71074
|
const host = parsed.hostname.toLowerCase();
|
|
71075
71075
|
return FACEBOOK_HOSTS.has(host) || host === "fb.watch";
|
|
71076
71076
|
}
|
|
71077
|
+
var AMAZON_STORE_TLDS = new Set([
|
|
71078
|
+
"com",
|
|
71079
|
+
"ca",
|
|
71080
|
+
"com.mx",
|
|
71081
|
+
"com.br",
|
|
71082
|
+
"co.uk",
|
|
71083
|
+
"de",
|
|
71084
|
+
"fr",
|
|
71085
|
+
"it",
|
|
71086
|
+
"es",
|
|
71087
|
+
"nl",
|
|
71088
|
+
"se",
|
|
71089
|
+
"pl",
|
|
71090
|
+
"com.be",
|
|
71091
|
+
"com.tr",
|
|
71092
|
+
"ie",
|
|
71093
|
+
"ae",
|
|
71094
|
+
"sa",
|
|
71095
|
+
"eg",
|
|
71096
|
+
"in",
|
|
71097
|
+
"co.jp",
|
|
71098
|
+
"sg",
|
|
71099
|
+
"com.au",
|
|
71100
|
+
"co.za"
|
|
71101
|
+
]);
|
|
71102
|
+
var AMAZON_SHORT_HOSTS = new Set(["amzn.to", "amzn.eu", "amzn.asia", "a.co"]);
|
|
71103
|
+
function isAmazonUrl(url) {
|
|
71104
|
+
let parsed;
|
|
71105
|
+
try {
|
|
71106
|
+
parsed = new URL(url);
|
|
71107
|
+
} catch {
|
|
71108
|
+
return false;
|
|
71109
|
+
}
|
|
71110
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
71111
|
+
if (AMAZON_SHORT_HOSTS.has(host))
|
|
71112
|
+
return true;
|
|
71113
|
+
return host.startsWith("amazon.") && AMAZON_STORE_TLDS.has(host.slice(7));
|
|
71114
|
+
}
|
|
71115
|
+
function resolveAmazonAppLink(url) {
|
|
71116
|
+
if (!isAmazonUrl(url))
|
|
71117
|
+
return null;
|
|
71118
|
+
const parsed = new URL(url);
|
|
71119
|
+
const asin = parsed.pathname.match(/^\/gp\/aw\/d\/([A-Z0-9]{10})(?:\/|$)/i)?.[1];
|
|
71120
|
+
return asin ? `${parsed.origin}/dp/${asin}` : null;
|
|
71121
|
+
}
|
|
71122
|
+
function extractAmazonImage(html) {
|
|
71123
|
+
const tag = html?.match(/<img[^>]*\bid="landingImage"[^>]*>/)?.[0];
|
|
71124
|
+
return tag?.match(/\bdata-old-hires="(https:[^"]+)"/)?.[1];
|
|
71125
|
+
}
|
|
71126
|
+
function schemaOrgSummary(jsonLD, title = "") {
|
|
71127
|
+
if (!Array.isArray(jsonLD))
|
|
71128
|
+
return;
|
|
71129
|
+
const items = jsonLD.flatMap((item) => item?.["@graph"] ?? item);
|
|
71130
|
+
const description = items.find((item) => typeof item?.description === "string" && item.description.trim())?.description;
|
|
71131
|
+
if (description)
|
|
71132
|
+
return description;
|
|
71133
|
+
for (const item of items) {
|
|
71134
|
+
const headline = typeof item?.headline === "string" ? item.headline.trim() : "";
|
|
71135
|
+
if (headline && !title.includes(headline))
|
|
71136
|
+
return headline;
|
|
71137
|
+
}
|
|
71138
|
+
}
|
|
71077
71139
|
function stripFacebookEngagement(title) {
|
|
71078
71140
|
const bar = title?.indexOf("|") ?? -1;
|
|
71079
71141
|
if (title === undefined || bar === -1) {
|
|
@@ -71189,7 +71251,26 @@ function redditPostImage(post) {
|
|
|
71189
71251
|
}
|
|
71190
71252
|
];
|
|
71191
71253
|
}
|
|
71192
|
-
return;
|
|
71254
|
+
return redditGalleryImages(post);
|
|
71255
|
+
}
|
|
71256
|
+
function redditGalleryImages(post) {
|
|
71257
|
+
const items = post.gallery_data?.items;
|
|
71258
|
+
if (!Array.isArray(items))
|
|
71259
|
+
return;
|
|
71260
|
+
const images = [];
|
|
71261
|
+
for (const item of items) {
|
|
71262
|
+
const id = item?.media_id;
|
|
71263
|
+
const source = typeof id === "string" ? post.media_metadata?.[id]?.s : undefined;
|
|
71264
|
+
const url = redditMediaUrl(source?.u ?? source?.gif, new Set(["i.redd.it", "preview.redd.it"]));
|
|
71265
|
+
if (url) {
|
|
71266
|
+
images.push({
|
|
71267
|
+
url,
|
|
71268
|
+
width: finiteNonNegative(source?.x),
|
|
71269
|
+
height: finiteNonNegative(source?.y)
|
|
71270
|
+
});
|
|
71271
|
+
}
|
|
71272
|
+
}
|
|
71273
|
+
return images.length ? images : undefined;
|
|
71193
71274
|
}
|
|
71194
71275
|
function redditPostVideo(post) {
|
|
71195
71276
|
const video = post.secure_media?.reddit_video;
|
|
@@ -71281,10 +71362,12 @@ function tweetToPageObject(status) {
|
|
|
71281
71362
|
|
|
71282
71363
|
`) : undefined;
|
|
71283
71364
|
const title = typeof article?.title === "string" && article.title || (author.name && author.screen_name ? `${author.name} (@${author.screen_name}) on X` : author.name ?? "Post on X");
|
|
71284
|
-
const
|
|
71285
|
-
const
|
|
71365
|
+
const media = tweet.media?.photos?.length || tweet.media?.videos?.length ? tweet.media : tweet.quote?.media;
|
|
71366
|
+
const photo = media?.photos?.[0];
|
|
71367
|
+
const video = media?.videos?.[0];
|
|
71286
71368
|
const articleImage = article?.cover_media?.media_info;
|
|
71287
|
-
const
|
|
71369
|
+
const card = tweet.card?.image;
|
|
71370
|
+
const imageUrl = articleImage?.original_img_url ?? photo?.url ?? video?.thumbnail_url ?? card?.url ?? author.avatar_url;
|
|
71288
71371
|
const page = {
|
|
71289
71372
|
type: "page",
|
|
71290
71373
|
title,
|
|
@@ -71297,8 +71380,8 @@ function tweetToPageObject(status) {
|
|
|
71297
71380
|
page.image = [
|
|
71298
71381
|
{
|
|
71299
71382
|
url: imageUrl,
|
|
71300
|
-
width: articleImage?.original_img_width ?? photo?.width ?? video?.width,
|
|
71301
|
-
height: articleImage?.original_img_height ?? photo?.height ?? video?.height
|
|
71383
|
+
width: articleImage?.original_img_width ?? photo?.width ?? video?.width ?? card?.width,
|
|
71384
|
+
height: articleImage?.original_img_height ?? photo?.height ?? video?.height ?? card?.height
|
|
71302
71385
|
}
|
|
71303
71386
|
];
|
|
71304
71387
|
}
|
|
@@ -71425,13 +71508,15 @@ var PlatformMetadataSchema = {
|
|
|
71425
71508
|
// src/index.ts
|
|
71426
71509
|
var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; SockethubBot/${package_default.version}; +https://sockethub.org)`;
|
|
71427
71510
|
var COMPAT_USER_AGENT = "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)";
|
|
71511
|
+
var AMAZON_USER_AGENT = "WhatsApp/2.23.20.0 A";
|
|
71512
|
+
var AMAZON_SCRAPE_TIMEOUT_MS = 1e4;
|
|
71428
71513
|
var TWEET_API_TIMEOUT_MS = 1e4;
|
|
71429
71514
|
var REDDIT_OEMBED_URL = "https://www.reddit.com/oembed";
|
|
71430
71515
|
var REDDIT_OEMBED_TIMEOUT_MS = 4000;
|
|
71431
71516
|
var YOUTUBE_OEMBED_TIMEOUT_MS = 4000;
|
|
71432
71517
|
var FACEBOOK_EMBED_TIMEOUT_MS = 4000;
|
|
71433
71518
|
var SCRAPE_TIMEOUT_MS = 5000;
|
|
71434
|
-
var REDDIT_JSON_TIMEOUT_MS =
|
|
71519
|
+
var REDDIT_JSON_TIMEOUT_MS = 5000;
|
|
71435
71520
|
var REDDIT_JSON_MAX_BYTES = 1e6;
|
|
71436
71521
|
var DIRECT_IMAGE_PROBE_TIMEOUT_MS = 5000;
|
|
71437
71522
|
var FACEBOOK_TRACKING_PARAMS = ["_fb_noscript", "fbclid", "mibextid"];
|
|
@@ -71551,7 +71636,8 @@ class Metadata {
|
|
|
71551
71636
|
this.fetchDirectImage(job, image, cb);
|
|
71552
71637
|
return;
|
|
71553
71638
|
}
|
|
71554
|
-
|
|
71639
|
+
const amazonUrl = resolveAmazonAppLink(job.actor.id);
|
|
71640
|
+
this.scrape(job, cb, undefined, amazonUrl ?? job.actor.id);
|
|
71555
71641
|
}
|
|
71556
71642
|
async fetchDirectImage(job, candidate, cb, timeoutMs = DIRECT_IMAGE_PROBE_TIMEOUT_MS) {
|
|
71557
71643
|
try {
|
|
@@ -71793,13 +71879,15 @@ class Metadata {
|
|
|
71793
71879
|
scrape(job, cb, redditEmbed, scrapeUrl = job.actor.id, youtubeEmbed) {
|
|
71794
71880
|
const youtubeVideo = Boolean(resolveYouTubeOEmbed(job.actor.id));
|
|
71795
71881
|
const useCompatUserAgent = isRedditUrl(job.actor.id) || isFacebookUrl(job.actor.id) || youtubeVideo;
|
|
71796
|
-
const
|
|
71882
|
+
const amazon = isAmazonUrl(job.actor.id);
|
|
71883
|
+
const scrapeTimeoutMs = amazon ? AMAZON_SCRAPE_TIMEOUT_MS : SCRAPE_TIMEOUT_MS;
|
|
71884
|
+
const userAgent = amazon ? AMAZON_USER_AGENT : useCompatUserAgent ? this.compatUserAgent() : this.userAgent();
|
|
71797
71885
|
const dispatcher = this.getDispatcher();
|
|
71798
71886
|
let youtubeDescription;
|
|
71799
71887
|
this.log.debug(`scrape started for ${job.actor.id} via ${scrapeUrl}`);
|
|
71800
71888
|
const options = {
|
|
71801
71889
|
url: scrapeUrl,
|
|
71802
|
-
timeout:
|
|
71890
|
+
timeout: scrapeTimeoutMs / 1000,
|
|
71803
71891
|
urlValidatorSettings: {
|
|
71804
71892
|
allow_fragments: true,
|
|
71805
71893
|
allow_protocol_relative_urls: false,
|
|
@@ -71833,7 +71921,7 @@ class Metadata {
|
|
|
71833
71921
|
}
|
|
71834
71922
|
return import_open_graph_scraper.default({ html });
|
|
71835
71923
|
}) : import_open_graph_scraper.default(options);
|
|
71836
|
-
withDeadline(scrape,
|
|
71924
|
+
withDeadline(scrape, scrapeTimeoutMs).then(async (data) => {
|
|
71837
71925
|
let { result } = data;
|
|
71838
71926
|
this.log.debug(`scrape completed for ${job.actor.id}`);
|
|
71839
71927
|
const reddit = isRedditUrl(job.actor.id);
|
|
@@ -71874,6 +71962,10 @@ class Metadata {
|
|
|
71874
71962
|
}
|
|
71875
71963
|
}
|
|
71876
71964
|
}
|
|
71965
|
+
const amazonImage = amazon && !result.ogImage?.[0]?.url?.includes("/images/I/") && extractAmazonImage(data.html);
|
|
71966
|
+
if (amazonImage) {
|
|
71967
|
+
result = { ...result, ogImage: [{ url: amazonImage }] };
|
|
71968
|
+
}
|
|
71877
71969
|
const embed = reddit ? await redditEmbed : undefined;
|
|
71878
71970
|
const youtube = await youtubeEmbed;
|
|
71879
71971
|
if (!reddit)
|
|
@@ -71889,7 +71981,7 @@ class Metadata {
|
|
|
71889
71981
|
language: result.ogLocale,
|
|
71890
71982
|
title: embed?.title ?? youtube?.title ?? (facebook ? stripFacebookEngagement(result.ogTitle) : result.ogTitle),
|
|
71891
71983
|
name: embed?.provider_name ?? youtube?.provider_name ?? siteName,
|
|
71892
|
-
description: normalizeDescription(youtubeDescription || result.ogDescription || ""),
|
|
71984
|
+
description: normalizeDescription(youtubeDescription || result.ogDescription || schemaOrgSummary(result.jsonLD, result.ogTitle) || ""),
|
|
71893
71985
|
image: reddit ? redditPostImages(result.ogImage) ?? redditOEmbedImage(embed ?? {}) : scrapedImages?.length ? scrapedImages : youtubeOEmbedImage(youtube),
|
|
71894
71986
|
url: job.actor.id,
|
|
71895
71987
|
favicon: result.favicon || "/favicon.ico",
|
|
@@ -71934,6 +72026,25 @@ class Metadata {
|
|
|
71934
72026
|
cb(null, job);
|
|
71935
72027
|
return;
|
|
71936
72028
|
}
|
|
72029
|
+
let filename = "";
|
|
72030
|
+
if (/must scrape an html page/i.test(err.message)) {
|
|
72031
|
+
try {
|
|
72032
|
+
filename = decodeURIComponent(new URL(job.actor.id).pathname.split("/").at(-1) || "");
|
|
72033
|
+
} catch {
|
|
72034
|
+
filename = job.actor.id.split(/[?#]/, 1)[0].split("/").at(-1) || "";
|
|
72035
|
+
}
|
|
72036
|
+
}
|
|
72037
|
+
if (filename) {
|
|
72038
|
+
job.object = {
|
|
72039
|
+
type: "page",
|
|
72040
|
+
title: filename,
|
|
72041
|
+
description: "",
|
|
72042
|
+
url: job.actor.id,
|
|
72043
|
+
favicon: "/favicon.ico"
|
|
72044
|
+
};
|
|
72045
|
+
cb(null, job);
|
|
72046
|
+
return;
|
|
72047
|
+
}
|
|
71937
72048
|
cb(markExpectedError(new Error(`metadata scrape failed for ${job.actor.id}: ${err.message}`)));
|
|
71938
72049
|
});
|
|
71939
72050
|
}
|
|
@@ -71950,4 +72061,4 @@ export {
|
|
|
71950
72061
|
withDeadline
|
|
71951
72062
|
};
|
|
71952
72063
|
|
|
71953
|
-
//# debugId=
|
|
72064
|
+
//# debugId=256C32E14BF4C7F264756E2164756E21
|