Skip to content

Commit 3338836

Browse files
committed
feat: Extract screenshots from CWS and Firefox addons
1 parent 74249c1 commit 3338836

File tree

11 files changed

+137
-4
lines changed

11 files changed

+137
-4
lines changed

.env

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SERVER_ORIGIN=http://localhost:3000

bun.lockb

340 Bytes
Binary file not shown.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"dataloader": "^2.2.2",
2020
"graphql": "^16.8.0",
2121
"linkedom": "^0.15.3",
22-
"picocolors": "^1.0.0"
22+
"picocolors": "^1.0.0",
23+
"radix3": "^1.1.2"
2324
},
2425
"devDependencies": {
2526
"@aklinker1/check": "^1.2.0",

src/apis/firefox-api.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import consola from "consola";
2+
import { buildScreenshotUrl } from "../utils/urls";
23

34
export function createFirefoxApiClient() {
45
return {
@@ -29,6 +30,13 @@ export function createFirefoxApiClient() {
2930
storeUrl: json.url,
3031
version: json.current_version.version,
3132
dailyActiveUsers: json.average_daily_users,
33+
screenshots: (json.previews as any[]).map<Gql.Screenshot>(
34+
(preview, i) => ({
35+
index: i,
36+
rawUrl: preview.image_url,
37+
indexUrl: buildScreenshotUrl("firefox", json.id, i),
38+
}),
39+
),
3240
};
3341
},
3442
};

src/crawlers/chrome-crawler.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import consola from "consola";
22
import { HTMLAnchorElement, HTMLElement, parseHTML } from "linkedom";
3+
import { buildScreenshotUrl } from "../utils/urls";
34

45
export async function crawlExtension(
56
id: string,
@@ -21,7 +22,7 @@ export async function crawlExtension(
2122
const { document } = parseHTML(html);
2223

2324
// Uncomment to debug HTML
24-
// Bun.write("chrome.html", document.documentElement.outerHTML);
25+
Bun.write("chrome.html", document.documentElement.outerHTML);
2526

2627
// Basic metadata
2728
const name = metaContent(document, "property=og:title")?.replace(
@@ -106,6 +107,23 @@ export async function crawlExtension(
106107
// const rating = extractNumber(ratingDiv.title); // "Average rating: 4.78 stars"
107108
// const reviewCount = extractNumber(ratingDiv.textContent); // "(1024)"
108109

110+
// <div
111+
// aria-label="Item media 1 screenshot"
112+
// data-media-url="https://lh3.googleusercontent.com/GUgh0ThX2FDPNvbaumYl4DqsUhsbYiCe-Hut9FoVEnkmTrXyA-sHbMk5jmZTj_t-dDP8rAmy6X6a6GNTCn9F8zo4VYU"
113+
// data-is-video="false"
114+
// data-slide-index="0"
115+
// >
116+
const screenshots = [...document.querySelectorAll("div[data-media-url]")]
117+
.filter((div) => div.getAttribute("data-is-video") === "false")
118+
.map<Gql.Screenshot>((div) => {
119+
const index = Number(div.getAttribute("data-slide-index") || -1);
120+
return {
121+
index,
122+
rawUrl: div.getAttribute("data-media-url") + "=s1280", // "s1280" gets the full resolution
123+
indexUrl: buildScreenshotUrl("chrome", id, index),
124+
};
125+
});
126+
109127
if (name == null) return;
110128
if (storeUrl == null) return;
111129
if (iconUrl == null) return;
@@ -114,6 +132,12 @@ export async function crawlExtension(
114132
if (version == null) return;
115133
if (shortDescription == null) return;
116134
if (longDescription == null) return;
135+
if (
136+
screenshots.some(
137+
(screenshot) => screenshot.index === -1 || !screenshot.rawUrl,
138+
)
139+
)
140+
return;
117141

118142
const result: Gql.ChromeExtension = {
119143
id,
@@ -127,6 +151,7 @@ export async function crawlExtension(
127151
longDescription,
128152
rating,
129153
reviewCount,
154+
screenshots,
130155
};
131156
consola.debug("Crawl results:", result);
132157
return result;

src/rest/getChromeScreenshot.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ChromeService } from "../services/chrome-service";
2+
import { RouteHandler } from "../utils/rest-router";
3+
4+
export const getChromeScreenshot =
5+
(chrome: ChromeService): RouteHandler<{ id: string; index: string }> =>
6+
async (params) => {
7+
const extension = await chrome.getExtension(params.id);
8+
const index = Number(params.index);
9+
const screenshot = extension?.screenshots.find(
10+
(screenshot) => screenshot.index == index,
11+
);
12+
13+
if (screenshot == null) return new Response(null, { status: 404 });
14+
return Response.redirect(screenshot.rawUrl);
15+
};

src/schema.gql

+17
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type ChromeExtension {
3333
lastUpdated: String!
3434
rating: Float
3535
reviewCount: Int
36+
screenshots: [Screenshot!]!
3637
}
3738

3839
type FirefoxAddon {
@@ -47,4 +48,20 @@ type FirefoxAddon {
4748
lastUpdated: String!
4849
rating: Float
4950
reviewCount: Int
51+
screenshots: [Screenshot!]!
52+
}
53+
54+
type Screenshot {
55+
"""
56+
The screenshot's order.
57+
"""
58+
index: Int!
59+
"""
60+
The image's raw URL provided by the service. When screenshots are updated, this URL changes.
61+
"""
62+
rawUrl: String!
63+
"""
64+
URL to the image based on the index. If the raw URL changes, the `indexUrl` will remain constant, good for links in README.md files.
65+
"""
66+
indexUrl: String!
5067
}

src/server.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import playgroundHtmlTemplate from "./public/playground.html";
55
import consola from "consola";
66
import { createChromeService } from "./services/chrome-service";
77
import { createFirefoxService } from "./services/firefox-service";
8+
import { createRestRouter } from "./utils/rest-router";
9+
import { getChromeScreenshot } from "./rest/getChromeScreenshot";
810

911
const playgroundHtml = playgroundHtmlTemplate.replace(
1012
"{{VERSION}}",
@@ -22,6 +24,11 @@ export function createServer(config?: ServerConfig) {
2224
firefox,
2325
});
2426

27+
const restRouter = createRestRouter().get(
28+
"/api/rest/chrome/:id/screenshots/:index",
29+
getChromeScreenshot(chrome),
30+
);
31+
2532
const httpServer = Bun.serve({
2633
port,
2734
error(request) {
@@ -32,8 +39,16 @@ export function createServer(config?: ServerConfig) {
3239
return createResponse(undefined, { status: 204 });
3340
}
3441

35-
// GraphQL
36-
if (req.url.endsWith("/api")) {
42+
const url = new URL(req.url, process.env.SERVER_ORIGIN);
43+
44+
// REST
45+
46+
if (url.pathname.startsWith("/api/rest")) {
47+
return restRouter.fetch(url, req);
48+
}
49+
50+
if (url.pathname.startsWith("/api")) {
51+
// GraphQL
3752
const data = await graphql.evaluateQuery(req);
3853

3954
return createResponse(JSON.stringify(data), {

src/services/chrome-service.ts

+2
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ export function createChromeService() {
2929
},
3030
};
3131
}
32+
33+
export type ChromeService = ReturnType<typeof createChromeService>;

src/utils/rest-router.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as radix3 from "radix3";
2+
3+
export type RouteHandler<TParams = {}> = (
4+
params: TParams,
5+
url: URL,
6+
req: Request,
7+
) => Response | Promise<Response>;
8+
9+
export interface Route {
10+
method: string;
11+
handler: RouteHandler;
12+
}
13+
14+
export function createRestRouter() {
15+
const r = radix3.createRouter<Route>();
16+
const router = {
17+
get(path: string, handler: RouteHandler) {
18+
r.insert(path, { method: "GET", handler });
19+
return router;
20+
},
21+
post(path: string, handler: RouteHandler) {
22+
r.insert(path, { method: "POST", handler });
23+
return router;
24+
},
25+
any(path: string, handler: RouteHandler) {
26+
r.insert(path, { method: "ANY", handler });
27+
return router;
28+
},
29+
on(method: string, path: string, handler: RouteHandler) {
30+
r.insert(path, { method, handler });
31+
return router;
32+
},
33+
async fetch(url: URL, req: Request): Promise<Response> {
34+
const match = r.lookup(url.pathname);
35+
if (match && (req.method === match.method || match.method === "ANY")) {
36+
return await match.handler(match.params ?? {}, url, req);
37+
}
38+
return new Response(null, { status: 404 });
39+
},
40+
};
41+
return router;
42+
}

src/utils/urls.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function buildScreenshotUrl(
2+
type: "chrome" | "firefox",
3+
id: string,
4+
index: number,
5+
) {
6+
return `${process.env.SERVER_ORIGIN}/api/rest/${type}/${id}/screenshots/${index}`;
7+
}

0 commit comments

Comments
 (0)