From 61d8079e6f20ab02e490fb4a75eaa047991474d0 Mon Sep 17 00:00:00 2001 From: Chuwen Date: Sat, 6 Apr 2024 00:35:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(route):=20Add=20Shopify=20App=20store=20?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E3=80=81App=20reviews=20(#14819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(route): 添加 Shopify App store 搜索、App reviews * feat: remove unused var `index` * Update lib/routes/shopify/apps/[handle].reviews.ts Co-authored-by: Tony * chore: auto build * style: auto format * fix: cr --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- lib/routes/shopify/apps/[handle].reviews.ts | 78 ++++++++++++++++++++ lib/routes/shopify/apps/const.ts | 1 + lib/routes/shopify/apps/search.ts | 82 +++++++++++++++++++++ lib/routes/shopify/namespace.ts | 6 ++ 4 files changed, 167 insertions(+) create mode 100644 lib/routes/shopify/apps/[handle].reviews.ts create mode 100644 lib/routes/shopify/apps/const.ts create mode 100644 lib/routes/shopify/apps/search.ts create mode 100644 lib/routes/shopify/namespace.ts diff --git a/lib/routes/shopify/apps/[handle].reviews.ts b/lib/routes/shopify/apps/[handle].reviews.ts new file mode 100644 index 0000000000..f0e796ed14 --- /dev/null +++ b/lib/routes/shopify/apps/[handle].reviews.ts @@ -0,0 +1,78 @@ +import { Data, DataItem, Route } from '@/types'; +import { Context } from 'hono'; +import { baseURL } from './const'; +import got from '@/utils/got'; +import { load } from 'cheerio'; + +export const route: Route = { + path: '/apps/:handle/reviews/:page?', + example: '/shopify/apps/flow/reviews', + parameters: { handle: '例如一个 App 的链接 https://apps.shopify.com/flow,其中 flow 就是指的是 handle' }, + name: 'App reviews', + maintainers: ['PrintNow'], + handler, + radar: [ + { + source: ['apps.shopify.com/:handle'], + }, + ], +}; + +async function handler(ctx: Context): Promise { + const { handle = '', page = '1' } = ctx.req.param(); + const response = await got.get(`${baseURL}/${handle}/reviews`, { + searchParams: { + sort_by: 'newest', + page, + }, + headers: { + accept: 'text/html, application/xhtml+xml', + 'accept-language': 'en-US;q=0.9', + referer: baseURL, + dnt: '1', + }, + }); + + const htmlContent = response.data; + const $ = load(htmlContent); + + const items = $('div[data-merchant-review]') + .toArray() + .map((item) => { + const $item = $(item); + + const reviewID = $item.attr('data-review-content-id'); + + const $review1 = $item.find('div:nth-child(1)'); + const $review2 = $item.find('div:nth-child(2)'); + + const description = $item.find('div[data-truncate-review] div[data-truncate-content-copy] p').html() || ''; + const author = $review2.find('div.tw-text-fg-primary').text().trim(); + + const result: DataItem = { + guid: reviewID, + title: description, + author, + pubDate: new Date($review1.find('div[aria-label] + div').text().trim()), + + // 评论内容 + description, + + _extra: { + ratting_value: Number($review1.find('div[role="img"]').attr('aria-label')?.substring(0, 1)), + location: $review2.find('div.tw-text-fg-primary + div').text().trim(), + author, + }, + }; + + return result; + }); + + return { + title: `Reviews handle:${handle} page:${page} – Shopify App Store`, + link: `${baseURL}/${handle}/reviews`, + allowEmpty: true, + language: 'en-US', + item: items, + }; +} diff --git a/lib/routes/shopify/apps/const.ts b/lib/routes/shopify/apps/const.ts new file mode 100644 index 0000000000..21b1e3f6df --- /dev/null +++ b/lib/routes/shopify/apps/const.ts @@ -0,0 +1 @@ +export const baseURL = 'https://apps.shopify.com'; diff --git a/lib/routes/shopify/apps/search.ts b/lib/routes/shopify/apps/search.ts new file mode 100644 index 0000000000..b8388efc3d --- /dev/null +++ b/lib/routes/shopify/apps/search.ts @@ -0,0 +1,82 @@ +import { Data, DataItem, Route } from '@/types'; +import type { Context } from 'hono'; +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { baseURL } from './const'; + +export const route: Route = { + path: '/apps/search/:q', + example: '/shopify/apps/search/flow', + parameters: { q: '需要搜索的 App' }, + name: 'App store search', + maintainers: ['PrintNow'], + handler, + radar: [ + { + source: ['apps.shopify.com/search'], + target: (_params, url) => { + const { searchParams } = new URL(url).searchParams; + return searchParams.has('q') ? `/shopify/apps/search/${searchParams.get('q')}` : null; + }, + }, + ], +}; + +async function handler(ctx: Context): Promise { + const { q = '' } = ctx.req.param(); + const response = await got.get(`${baseURL}/search`, { + searchParams: { + q, + }, + headers: { + accept: 'text/html, application/xhtml+xml', + 'accept-language': 'en-US;q=0.9', + 'turbo-frame': 'search_page', + referer: baseURL, + dnt: '1', + }, + }); + + const htmlContent = await response.data; + const $ = load(htmlContent); + + const items = $('.search-results-component div[data-controller="app-card"]') + .toArray() + .map((item) => { + const handle = $(item).attr('data-app-card-handle-value'); + + // const appInfo = $(item).find('div.tw-transition-colors.tw-text-fg-primary + div.tw-self-stretch'); + const appInfo = $(item).find('div.tw-self-stretch').clone(); + + const rattingMatch = appInfo + .find('span') + .text() + .match(/\d\.\d/); + const rattingCountMatch = appInfo.find('span + span.tw-sr-only').text().match(/\d+/); + + const result: DataItem = { + title: $(item).attr('data-app-card-name-value') ?? '', + link: `${baseURL}/${handle}`, + description: $(item).find(`div.tw-text-fg-tertiary`).first().text().trim(), + image: $(item).attr('data-app-card-icon-url-value'), + _extra: { + handle, + description: $(item).find(`div.tw-text-fg-tertiary`).first().text().trim(), + built_for_shopify: $(item).find(`span.built-for-shopify-badge`).length > 0, + ratting: rattingMatch ? Number.parseFloat(rattingMatch[0]) : 0, + ratting_count: rattingCountMatch ? Number(rattingCountMatch[0]) : 0, + }, + }; + + return result; + }); + + return { + title: `Search results for "${q}" – Shopify App Store`, + link: `https://apps.shopify.com/search?q=${q}`, + // description: `Search results for "${q}" – Shopify App Store`, + allowEmpty: true, + language: 'en-US', + item: items, + }; +} diff --git a/lib/routes/shopify/namespace.ts b/lib/routes/shopify/namespace.ts new file mode 100644 index 0000000000..6d971caef4 --- /dev/null +++ b/lib/routes/shopify/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Shopify', + url: 'shopify.com', +};