@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 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. If the API is unavailable or the
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.3",
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.3",
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.3"
70827
+ "@sockethub/schemas": "^3.0.0-beta.5"
70828
70828
  },
70829
70829
  peerDependencies: {
70830
- "@sockethub/server": "^5.0.0-beta.3"
70830
+ "@sockethub/server": "^5.0.0-beta.5"
70831
70831
  },
70832
- gitHead: "bc04522d60d1e2fa1b3098b339897de62c3b0549",
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 photo = tweet.media?.photos?.[0];
71285
- const video = tweet.media?.videos?.[0];
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 imageUrl = articleImage?.original_img_url ?? photo?.url ?? video?.thumbnail_url;
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 = 2500;
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
- this.scrape(job, cb);
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 userAgent = useCompatUserAgent ? this.compatUserAgent() : this.userAgent();
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: SCRAPE_TIMEOUT_MS / 1000,
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, SCRAPE_TIMEOUT_MS).then(async (data) => {
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=E83893E42765EF6064756E2164756E21
72064
+ //# debugId=256C32E14BF4C7F264756E2164756E21