diff options
Diffstat (limited to 'cgithub/src/scraper.ts')
| -rw-r--r-- | cgithub/src/scraper.ts | 126 |
1 files changed, 43 insertions, 83 deletions
diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index 223839f..4220dc4 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -143,7 +143,17 @@ interface CodeViewBlobRoute { richText: string | null; } +interface SidebarAbout { + description?: string | null; + website?: string | null; + stargazerCount?: number; + forksCount?: number; + // A section is false when the repository has none of that thing. + sections?: { releases?: { releaseCount?: number } | false }; +} + interface AppPayload { + sidebarAbout?: SidebarAbout; codeViewLayoutRoute?: CodeViewLayoutRoute; codeViewRepoRoute?: CodeViewRepoRoute; codeViewTreeRoute?: CodeViewTreeRoute; @@ -230,8 +240,6 @@ export interface RepoInfo { stars: string | null; forks: string | null; numReleases?: number; - latestReleaseTag?: string; - latestReleaseDate?: string; } export interface GitHubRepo extends GitHubTree { @@ -292,8 +300,14 @@ export interface Language { color: string | null; } -export interface GitHubLanguages extends GitHubCommon { +export interface LatestRelease { + tag: string; + publishedAt: string | null; +} + +export interface GitHubSidebar extends GitHubCommon { languages: Language[]; + latestRelease: LatestRelease | null; } export interface Ref { @@ -425,81 +439,6 @@ function parsePayload(html: string): AppPayload | null { return parseEmbeddedPayload<AppPayload>(html); } -function parseRepoInfo(html: string): RepoInfo { - const document = parseDocument(html); - - const aboutSection = cssSelect.selectOne(".BorderGrid-cell h2:contains('About')", document); - let description: string | null = null; - let website: string | null = null; - - if (aboutSection) { - const parent = aboutSection.parent; - if (parent) { - const descElem = cssSelect.selectOne("p.f4", parent); - if (descElem) { - description = textContent(descElem).trim(); - } - const websiteElem = cssSelect.selectOne( - "a[rel='noopener noreferrer nofollow']", - parent, - ) as Element | null; - if (websiteElem) { - website = getAttributeValue(websiteElem, "href") || null; - } - } - } - - const starsElem = cssSelect.selectOne("#repo-stars-counter-star", document) as Element | null; - const stars = starsElem - ? getAttributeValue(starsElem, "title") || textContent(starsElem).trim() - : null; - - const forksElem = cssSelect.selectOne("#repo-network-counter", document) as Element | null; - const forks = forksElem - ? getAttributeValue(forksElem, "title") || textContent(forksElem).trim() - : null; - - const releaseElem = cssSelect.selectOne( - "a[href$='/releases'] .Counter", - document, - ) as Element | null; - let numReleases: number | undefined; - let latestReleaseTag: string | undefined; - let latestReleaseDate: string | undefined; - - if (releaseElem) { - const releaseText = getAttributeValue(releaseElem, "title") || textContent(releaseElem); - numReleases = parseInt(releaseText.replace(/,/g, ""), 10); - - // Find the latest release tag and date if available - const cell = (releaseElem.parent as Element)?.parent?.parent as Element; // span -> a -> h2 -> cell - if (cell) { - const latestLink = cssSelect.selectOne("a[href*='/releases/tag/']", cell) as Element; - if (latestLink) { - const tagElem = cssSelect.selectOne(".css-truncate-target", latestLink); - if (tagElem) { - latestReleaseTag = textContent(tagElem).trim(); - } - - const timeElem = cssSelect.selectOne("relative-time", latestLink); - if (timeElem) { - latestReleaseDate = getAttributeValue(timeElem, "datetime") || undefined; - } - } - } - } - - return { - description, - website, - stars, - forks, - numReleases, - latestReleaseTag, - latestReleaseDate, - }; -} - function extractOverviewHtml( overviewFiles: OverviewFile[] = [], ): Record<string, string | null> | undefined { @@ -539,7 +478,15 @@ export async function getGitHubRepo(owner: string, repo: string): Promise<GitHub throw new Error("Could not find tree data in embedded JSON"); } - const info = parseRepoInfo(html); + const about = payload?.sidebarAbout; + const releases = about?.sections?.releases; + const info = { + description: about?.description ?? null, + website: about?.website ?? null, + stars: about?.stargazerCount?.toLocaleString("en-US") ?? null, + forks: about?.forksCount?.toLocaleString("en-US") ?? null, + numReleases: releases === false ? undefined : releases?.releaseCount, + }; const overviewHtml = extractOverviewHtml(repoRoute.overview?.overviewFiles); return extractGitHub(payload, { items: repoRoute.tree.items, info, overviewHtml }); @@ -742,11 +689,16 @@ interface SidebarPayload { color?: string; }[]; }; + releases?: { + // The name is the release title, which need not be the tag; the path is. + latestRelease?: { name?: string; path: string; publishedAt?: string } | null; + }; } -// The repository sidebar (languages, contributors, ...) is not rendered into -// the repo page at all; GitHub's own UI lazily fetches it as JSON from here. -export async function getGitHubLanguages(owner: string, repo: string): Promise<GitHubLanguages> { +// The repository sidebar (languages, releases, contributors, ...) is not +// rendered into the repo page at all; GitHub's own UI lazily fetches it as +// JSON from here. +export async function getGitHubSidebar(owner: string, repo: string): Promise<GitHubSidebar> { const payload = await fetchGitHubJson<SidebarPayload>(`${owner}/${repo}/_sidebar`); const languages: Language[] = []; @@ -758,7 +710,15 @@ export async function getGitHubLanguages(owner: string, repo: string): Promise<G }); } - return { repo: { owner, name: repo }, languages }; + const release = payload.releases?.latestRelease; + const latestRelease = release + ? { + tag: decodeURIComponent(release.path.split("/releases/tag/")[1] ?? release.name ?? ""), + publishedAt: release.publishedAt ?? null, + } + : null; + + return { repo: { owner, name: repo }, languages, latestRelease }; } interface BranchRefEntry { |
