crawlforge-extractors 1.1.0 → 1.2.1
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/package.json +1 -1
- package/src/templates.js +103 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-extractors",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Extraction logic shared by the CrawlForge MCP server and REST API — scrape templates, charset-correct capped body reading, and structural fingerprinting. One implementation, so the two surfaces cannot drift apart.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
package/src/templates.js
CHANGED
|
@@ -104,6 +104,33 @@ function tidy(value) {
|
|
|
104
104
|
* "by Jonathan Haidt (Author) Format: Hardcover" on a book. Each states the
|
|
105
105
|
* same fact wrapped in different chrome.
|
|
106
106
|
*/
|
|
107
|
+
function npmLicense(value) {
|
|
108
|
+
if (!value) return null;
|
|
109
|
+
if (typeof value === 'string') return value;
|
|
110
|
+
// Very old packages use { type, url } or an array of them.
|
|
111
|
+
if (Array.isArray(value)) return value.map(npmLicense).filter(Boolean).join(', ') || null;
|
|
112
|
+
return value.type || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function npmRepositoryUrl(repository) {
|
|
116
|
+
const raw = typeof repository === 'string' ? repository : repository?.url;
|
|
117
|
+
if (!raw) return null;
|
|
118
|
+
// Registry URLs come as git+ssh://git@host/o/r.git, git+https://…, git://…
|
|
119
|
+
// or a bare "owner/repo" shorthand. Normalise to a browsable https URL.
|
|
120
|
+
let url = raw.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
121
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(url)) return `https://github.com/${url}`;
|
|
122
|
+
url = url.replace(/^git@([^:]+):/, 'https://$1/');
|
|
123
|
+
url = url.replace(/^(?:git|ssh):\/\/(?:git@)?/, 'https://');
|
|
124
|
+
url = url.replace(/^https:\/\/git@/, 'https://');
|
|
125
|
+
return url;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function npmBugsUrl(bugs) {
|
|
129
|
+
if (!bugs) return null;
|
|
130
|
+
if (typeof bugs === 'string') return bugs;
|
|
131
|
+
return bugs.url || bugs.email || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
function amazonByline($) {
|
|
108
135
|
const contributor = tidy($('#bylineInfo .contributorNameID').first().text());
|
|
109
136
|
const raw = tidy($('#bylineInfo').first().text());
|
|
@@ -145,6 +172,22 @@ function fullSizeImage(src) {
|
|
|
145
172
|
return src.replace(/\._[^/]*_\.(jpe?g|png|gif)$/i, ".$1");
|
|
146
173
|
}
|
|
147
174
|
|
|
175
|
+
// ── YouTube helpers ──────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Views and likes are both userInteractionCount; only the sibling
|
|
179
|
+
* interactionType tells them apart, and YouTube emits the LikeAction counter
|
|
180
|
+
* first — so reading the attribute directly returns likes where views are
|
|
181
|
+
* meant. There is no interactionCount attribute on the page at all.
|
|
182
|
+
*/
|
|
183
|
+
function youtubeInteractionCount($, action) {
|
|
184
|
+
const counter = $('[itemprop="interactionStatistic"]')
|
|
185
|
+
.filter((_, el) => ($(el).find('[itemprop="interactionType"]').attr('content') || '').split('/').pop() === action)
|
|
186
|
+
.first();
|
|
187
|
+
const count = counter.find('[itemprop="userInteractionCount"]').attr('content');
|
|
188
|
+
return count ? Number.parseInt(count, 10) : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
148
191
|
// ── Template definitions ─────────────────────────────────────────────────────
|
|
149
192
|
|
|
150
193
|
export const TEMPLATES = [
|
|
@@ -333,7 +376,8 @@ export const TEMPLATES = [
|
|
|
333
376
|
title: attr($, 'meta[name="title"]', 'content') || attr($, 'meta[property="og:title"]', 'content'),
|
|
334
377
|
channel: attr($, 'link[itemprop="name"]', 'content') || text($, '#channel-name'),
|
|
335
378
|
channel_url: attr($, 'span[itemprop="author"] link[itemprop="url"]', 'href'),
|
|
336
|
-
views:
|
|
379
|
+
views: youtubeInteractionCount($, 'WatchAction'),
|
|
380
|
+
likes: youtubeInteractionCount($, 'LikeAction'),
|
|
337
381
|
published: attr($, 'meta[itemprop="uploadDate"]', 'content') || attr($, 'meta[itemprop="datePublished"]', 'content'),
|
|
338
382
|
description: attr($, 'meta[property="og:description"]', 'content'),
|
|
339
383
|
thumbnail: attr($, 'meta[property="og:image"]', 'content'),
|
|
@@ -466,25 +510,66 @@ export const TEMPLATES = [
|
|
|
466
510
|
{
|
|
467
511
|
id: 'npm-package',
|
|
468
512
|
name: 'npm Package',
|
|
469
|
-
description:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
513
|
+
description:
|
|
514
|
+
'Read a package from the npm registry API rather than the rendered npmjs.com page: exact ' +
|
|
515
|
+
'latest version, license, repository, homepage, maintainers, dependencies and any ' +
|
|
516
|
+
'deprecation notice. npmjs.com blocks plain HTTP fetches, and its markup carries no stable ' +
|
|
517
|
+
'hooks, so the page itself yields almost nothing. Weekly download counts are not included — ' +
|
|
518
|
+
'they live on a separate api.npmjs.org endpoint.',
|
|
519
|
+
targetPattern: /npmjs\.com\/package\/|registry\.npmjs\.org\//i,
|
|
520
|
+
|
|
521
|
+
/** Point the fetch at the registry document for the same package. */
|
|
522
|
+
resolveUrl(url) {
|
|
523
|
+
const parsed = new URL(url);
|
|
524
|
+
if (parsed.hostname === 'registry.npmjs.org') return url;
|
|
525
|
+
// /package/<name>, /package/@scope/<name>, either optionally followed by
|
|
526
|
+
// /v/<version> or /access etc.
|
|
527
|
+
const match = parsed.pathname.match(/\/package\/((?:@[^/]+\/)?[^/]+)/i);
|
|
528
|
+
if (!match) return url;
|
|
529
|
+
return `https://registry.npmjs.org/${match[1]}`;
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
extractRaw(body, url) {
|
|
533
|
+
let doc;
|
|
534
|
+
try {
|
|
535
|
+
doc = JSON.parse(body);
|
|
536
|
+
} catch {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`Not an npm registry document: ${url} did not return JSON. ` +
|
|
539
|
+
'This template reads the npm registry API.'
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// The registry answers an unknown package with {"error":"Not found"}.
|
|
544
|
+
if (!doc || typeof doc.name !== 'string') {
|
|
545
|
+
const reason = typeof doc?.error === 'string' ? doc.error : 'no package document';
|
|
546
|
+
throw new Error(`No npm package at ${url}: ${reason}.`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const latest = doc['dist-tags']?.latest || null;
|
|
550
|
+
const release = (latest && doc.versions?.[latest]) || {};
|
|
551
|
+
const deps = release.dependencies || {};
|
|
477
552
|
|
|
478
553
|
return {
|
|
479
|
-
name:
|
|
480
|
-
version:
|
|
481
|
-
description:
|
|
482
|
-
license:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
maintainers:
|
|
554
|
+
name: doc.name,
|
|
555
|
+
version: latest,
|
|
556
|
+
description: release.description || doc.description || null,
|
|
557
|
+
license: npmLicense(release.license ?? doc.license),
|
|
558
|
+
homepage: release.homepage || doc.homepage || null,
|
|
559
|
+
repository: npmRepositoryUrl(release.repository || doc.repository),
|
|
560
|
+
bugs: npmBugsUrl(release.bugs || doc.bugs),
|
|
561
|
+
keywords: release.keywords || doc.keywords || [],
|
|
562
|
+
maintainers: (doc.maintainers || [])
|
|
563
|
+
.map(m => (typeof m === 'string' ? m : m?.name))
|
|
564
|
+
.filter(Boolean),
|
|
565
|
+
dependencies: deps,
|
|
566
|
+
dependency_count: Object.keys(deps).length,
|
|
567
|
+
// A string when the publisher deprecated it, false otherwise — npm
|
|
568
|
+
// keeps serving deprecated packages, so this is the only signal.
|
|
569
|
+
deprecated: typeof release.deprecated === 'string' ? release.deprecated : false,
|
|
570
|
+
published: (latest && doc.time?.[latest]) || null,
|
|
571
|
+
last_modified: doc.time?.modified || null,
|
|
572
|
+
install_command: `npm install ${doc.name}`
|
|
488
573
|
};
|
|
489
574
|
}
|
|
490
575
|
}
|