From 1ffd371dfeb1ab0d7fbf1955bf610e128e201ffc Mon Sep 17 00:00:00 2001 From: surwall Date: Tue, 18 Nov 2025 05:32:10 +0800 Subject: [PATCH] feat(route): add Google Play Store (#20231) Co-authored-by: Xinhao Luo --- lib/routes/google/play.ts | 169 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 lib/routes/google/play.ts diff --git a/lib/routes/google/play.ts b/lib/routes/google/play.ts new file mode 100644 index 0000000000..f41e744543 --- /dev/null +++ b/lib/routes/google/play.ts @@ -0,0 +1,169 @@ +import { Route } from '@/types'; +import type { Context } from 'hono'; +import puppeteer from '@/utils/puppeteer'; +import logger from '@/utils/logger'; +import { parseDate } from '@/utils/parse-date'; +import { Browser } from 'rebrowser-puppeteer'; +import { load } from 'cheerio'; + +export const route: Route = { + name: 'Play Store Update', + path: '/play/:id/:lang?', + categories: ['program-update'], + example: '/google/play/net.dinglisch.android.taskerm', + parameters: { + id: 'Package id, can be found in url', + lang: { + description: 'language', + options: [ + { value: 'en-us', label: 'English' }, + { value: 'zh-cn', label: '简体中文' }, + ], + default: 'en-us', + }, + }, + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['play.google.com/store/apps/details?id=:id'], + }, + ], + maintainers: ['surwall'], + handler, +}; + +// check more language codes on https://support.google.com/googleplay/android-developer/table/4419860?hl=en +const keywords = { + // find aria-label of the button next to the "About this app" + aboutThisAppButton: { + 'en-us': 'See more information on About this app', + 'zh-cn': '查看“关于此应用”的更多相关信息', + }, + whatsNew: { + 'en-us': 'What’s new', + 'zh-cn': '新变化', + }, + updatedOn: { + 'en-us': 'Updated on', + 'zh-cn': '更新日期', + }, + // this format is used to parse the date string in the "Updated on" section + updatedOnFormat: { + // Jun 23, 2025, 'Jun' is a locale word, we use 'en' as the locale key + 'en-us': ['MMM D, YYYY', 'en'], + // 2025年6月23日 + 'zh-cn': ['YYYY年M月D日'], + }, + version: { + 'en-us': 'Version', + 'zh-cn': '版本', + }, + offeredBy: { + 'en-us': 'Offered by', + 'zh-cn': '提供方', + }, +}; + +async function handler(ctx: Context) { + const id = ctx.req.param('id'); + const lang = ctx.req.param('lang') ?? 'en-us'; + const baseurl = 'https://play.google.com/store/apps'; + const link = `${baseurl}/details?id=${id}&hl=${lang}`; + + let browser: Browser | undefined; + let htmlContent = ''; + try { + browser = await puppeteer(); + const page = await browser.newPage(); + page.setRequestInterception(true); + page.on('request', (req) => { + if (['image', 'font', 'stylesheet'].includes(req.resourceType())) { + req.abort(); + } else { + req.continue(); + } + }); + + logger.http(`Requesting ${link}`); + await page.goto(link, { + waitUntil: 'domcontentloaded', + }); + + // click "about this app" arrow button + const aboutThisAppButtonXpath = `::-p-xpath(//button[@aria-label='${keywords.aboutThisAppButton[lang]}'])`; + await page.click(aboutThisAppButtonXpath); + + // waiting for a dialog containing
Version
+ const versionXpath = `::-p-xpath(//div[text()="${keywords.version[lang]}"]/following-sibling::*[1])`; + await page.waitForSelector(versionXpath); + + htmlContent = await page.content(); + const $ = load(htmlContent); + + const appName = $('span[itemprop=name]').first().text(); + const appImage = $('img[itemprop=image]').first().attr('src'); + + let updatedOnStr: string | undefined; + let version: string | undefined; + let offeredBy: string | undefined; + + $('div').each(function () { + if ($(this).text().trim() === keywords.updatedOn[lang]) { + updatedOnStr = $(this).next().text().trim(); + } else if ($(this).text().trim() === keywords.version[lang]) { + version = $(this).next().text().trim(); + } else if ($(this).text().trim() === keywords.offeredBy[lang]) { + offeredBy = $(this).next().text().trim(); + } + }); + + if (!updatedOnStr || !version || !offeredBy) { + throw new Error('Failed to parse the page'); + } + + const updatedDate = parseDate(updatedOnStr, ...keywords.updatedOnFormat[lang]); + + const whatsNew = $('div[itemprop=description]').html(); + + const feedContent = ` +

${keywords.whatsNew[lang]}

+

${whatsNew ?? 'No release notes'}

+ `; + + return { + title: appName + ' - Google Play', + link, + image: appImage, + item: [ + { + title: formatVersion(version, updatedDate), + description: feedContent, + link, + pubDate: updatedDate, + guid: formatGuid(version, updatedDate), + author: offeredBy, + }, + ], + }; + } finally { + await browser?.close(); + } +} + +function formatVersion(version: string, updatedDate: Date) { + // some apps show version as "Varies with device" + // https://play.google.com/store/apps/details?id=com.adobe.reader&hl=en-us + const isVersion = /^\d/.test(version); + return isVersion ? version : updatedDate.toISOString().slice(0, 10); +} + +function formatGuid(version: string, updatedDate: Date) { + return updatedDate.getTime().toString() + '-' + version; +}