98 lines
3.9 KiB
JavaScript
98 lines
3.9 KiB
JavaScript
|
|
/**
|
||
|
|
* Discovery adapters.
|
||
|
|
*
|
||
|
|
* Each adapter returns a normalised shape:
|
||
|
|
* { name, platform, sourceUrl, usage, usageMetric, raw }
|
||
|
|
*
|
||
|
|
* Rules for an adapter:
|
||
|
|
* - it discovers SOFTWARE, it never scores it
|
||
|
|
* - it records where it found the thing and how much it is used
|
||
|
|
* - it fails loudly, never silently returning an empty list on a blocked fetch
|
||
|
|
*
|
||
|
|
* Sources currently live:
|
||
|
|
* github open API, no key needed, 60 requests/hour unauthenticated
|
||
|
|
* flathub open API, POST /api/v2/search, no key
|
||
|
|
*
|
||
|
|
* Sources deliberately not automated:
|
||
|
|
* alternativeto Cloudflare-protected. Readable through a real browser engine
|
||
|
|
* (verified 2026-10-29) but their robots.txt asks ai-train=no.
|
||
|
|
* Treated as a demand signal for manual/periodic import, not a
|
||
|
|
* crawler target. Do not add a scraper here without a decision.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const UA = 'bigtech-sucks-ingest/0.1 (+https://bigtech.sucks; assessment roster)';
|
||
|
|
|
||
|
|
async function getJson(url, init = {}) {
|
||
|
|
const res = await fetch(url, {
|
||
|
|
...init,
|
||
|
|
headers: { 'User-Agent': UA, Accept: 'application/json', ...(init.headers || {}) },
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`${url} -> HTTP ${res.status}`);
|
||
|
|
return res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------ relevance
|
||
|
|
// Usage rank alone is not a roster. `topic:messaging` sorts RabbitMQ to the top,
|
||
|
|
// because a message broker is highly starred and not a thing a human installs.
|
||
|
|
// A candidate must look like software a person uses, or it is dropped and the
|
||
|
|
// reason is recorded.
|
||
|
|
const NOT_AN_APP =
|
||
|
|
/\b(broker|queue|mqtt|amqp|kafka|nats|pulsar|rocketmq|emqx|nsq|rabbitmq|database|sdk|library|libraries|framework|protocol|server|infrastructure|plugin|extension|binding|middleware|gateway|proxy|driver|api client|spec|specification|awesome list)\b/i;
|
||
|
|
const APP_HINT =
|
||
|
|
/\b(app|application|client|desktop|chat|messenger|messaging|mobile|gui|editor|browser|vault|manager|suite)\b/i;
|
||
|
|
|
||
|
|
export function looksLikeUserSoftware(row) {
|
||
|
|
const text = `${row.name} ${row.raw?.description ?? ''}`;
|
||
|
|
if (NOT_AN_APP.test(text)) return false;
|
||
|
|
if (row.raw?.homepage) return true;
|
||
|
|
return APP_HINT.test(text);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------ GitHub
|
||
|
|
export async function githubByTopic(topic, { minStars = 500, limit = 10 } = {}) {
|
||
|
|
const q = encodeURIComponent(`topic:${topic} stars:>=${minStars} archived:false`);
|
||
|
|
const data = await getJson(
|
||
|
|
`https://api.github.com/search/repositories?q=${q}&sort=stars&order=desc&per_page=${limit}`,
|
||
|
|
);
|
||
|
|
return (data.items || []).map((r) => ({
|
||
|
|
name: r.name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
||
|
|
platform: 'source',
|
||
|
|
sourceUrl: r.html_url,
|
||
|
|
usage: r.stargazers_count ?? 0,
|
||
|
|
usageMetric: 'github_stars',
|
||
|
|
raw: {
|
||
|
|
full_name: r.full_name,
|
||
|
|
stars: r.stargazers_count,
|
||
|
|
forks: r.forks_count,
|
||
|
|
license: r.license?.spdx_id ?? null,
|
||
|
|
description: r.description,
|
||
|
|
homepage: r.homepage || null,
|
||
|
|
language: r.language,
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ------------------------------------------------------------------ Flathub
|
||
|
|
export async function flathubSearch(query, { limit = 10 } = {}) {
|
||
|
|
const data = await getJson('https://flathub.org/api/v2/search', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ query, filters: [] }),
|
||
|
|
});
|
||
|
|
return (data.hits || []).slice(0, limit).map((h) => ({
|
||
|
|
name: h.name,
|
||
|
|
platform: 'linux-flatpak',
|
||
|
|
sourceUrl: h.app_id ? `https://flathub.org/apps/${h.app_id}` : null,
|
||
|
|
// Flathub does not publish install counts in this endpoint. Presence in the
|
||
|
|
// distro store is a weak but real usage signal, scored low on purpose.
|
||
|
|
usage: 100,
|
||
|
|
usageMetric: 'flathub_presence',
|
||
|
|
raw: { app_id: h.app_id, summary: h.summary, developer: h.developer_name ?? null },
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
export const ADAPTERS = {
|
||
|
|
github: (topic) => githubByTopic(topic),
|
||
|
|
flathub: (query) => flathubSearch(query),
|
||
|
|
};
|