Skip to content

Commit 1ffd371

Browse files
surwallNeverBehave
andauthored
feat(route): add Google Play Store (#20231)
Co-authored-by: Xinhao Luo <neverbehave@fastmail.com>
1 parent ed51422 commit 1ffd371

File tree

1 file changed

+169
-0
lines changed

1 file changed

+169
-0
lines changed

lib/routes/google/play.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { Route } from '@/types';
2+
import type { Context } from 'hono';
3+
import puppeteer from '@/utils/puppeteer';
4+
import logger from '@/utils/logger';
5+
import { parseDate } from '@/utils/parse-date';
6+
import { Browser } from 'rebrowser-puppeteer';
7+
import { load } from 'cheerio';
8+
9+
export const route: Route = {
10+
name: 'Play Store Update',
11+
path: '/play/:id/:lang?',
12+
categories: ['program-update'],
13+
example: '/google/play/net.dinglisch.android.taskerm',
14+
parameters: {
15+
id: 'Package id, can be found in url',
16+
lang: {
17+
description: 'language',
18+
options: [
19+
{ value: 'en-us', label: 'English' },
20+
{ value: 'zh-cn', label: '简体中文' },
21+
],
22+
default: 'en-us',
23+
},
24+
},
25+
features: {
26+
requireConfig: false,
27+
requirePuppeteer: true,
28+
antiCrawler: false,
29+
supportBT: false,
30+
supportPodcast: false,
31+
supportScihub: false,
32+
},
33+
radar: [
34+
{
35+
source: ['play.google.com/store/apps/details?id=:id'],
36+
},
37+
],
38+
maintainers: ['surwall'],
39+
handler,
40+
};
41+
42+
// check more language codes on https://support.google.com/googleplay/android-developer/table/4419860?hl=en
43+
const keywords = {
44+
// find aria-label of the button next to the "About this app"
45+
aboutThisAppButton: {
46+
'en-us': 'See more information on About this app',
47+
'zh-cn': '查看“关于此应用”的更多相关信息',
48+
},
49+
whatsNew: {
50+
'en-us': 'What’s new',
51+
'zh-cn': '新变化',
52+
},
53+
updatedOn: {
54+
'en-us': 'Updated on',
55+
'zh-cn': '更新日期',
56+
},
57+
// this format is used to parse the date string in the "Updated on" section
58+
updatedOnFormat: {
59+
// Jun 23, 2025, 'Jun' is a locale word, we use 'en' as the locale key
60+
'en-us': ['MMM D, YYYY', 'en'],
61+
// 2025年6月23日
62+
'zh-cn': ['YYYY年M月D日'],
63+
},
64+
version: {
65+
'en-us': 'Version',
66+
'zh-cn': '版本',
67+
},
68+
offeredBy: {
69+
'en-us': 'Offered by',
70+
'zh-cn': '提供方',
71+
},
72+
};
73+
74+
async function handler(ctx: Context) {
75+
const id = ctx.req.param('id');
76+
const lang = ctx.req.param('lang') ?? 'en-us';
77+
const baseurl = 'https://play.google.com/store/apps';
78+
const link = `${baseurl}/details?id=${id}&hl=${lang}`;
79+
80+
let browser: Browser | undefined;
81+
let htmlContent = '';
82+
try {
83+
browser = await puppeteer();
84+
const page = await browser.newPage();
85+
page.setRequestInterception(true);
86+
page.on('request', (req) => {
87+
if (['image', 'font', 'stylesheet'].includes(req.resourceType())) {
88+
req.abort();
89+
} else {
90+
req.continue();
91+
}
92+
});
93+
94+
logger.http(`Requesting ${link}`);
95+
await page.goto(link, {
96+
waitUntil: 'domcontentloaded',
97+
});
98+
99+
// click "about this app" arrow button
100+
const aboutThisAppButtonXpath = `::-p-xpath(//button[@aria-label='${keywords.aboutThisAppButton[lang]}'])`;
101+
await page.click(aboutThisAppButtonXpath);
102+
103+
// waiting for a dialog containing <div class="xxxx">Version</div>
104+
const versionXpath = `::-p-xpath(//div[text()="${keywords.version[lang]}"]/following-sibling::*[1])`;
105+
await page.waitForSelector(versionXpath);
106+
107+
htmlContent = await page.content();
108+
const $ = load(htmlContent);
109+
110+
const appName = $('span[itemprop=name]').first().text();
111+
const appImage = $('img[itemprop=image]').first().attr('src');
112+
113+
let updatedOnStr: string | undefined;
114+
let version: string | undefined;
115+
let offeredBy: string | undefined;
116+
117+
$('div').each(function () {
118+
if ($(this).text().trim() === keywords.updatedOn[lang]) {
119+
updatedOnStr = $(this).next().text().trim();
120+
} else if ($(this).text().trim() === keywords.version[lang]) {
121+
version = $(this).next().text().trim();
122+
} else if ($(this).text().trim() === keywords.offeredBy[lang]) {
123+
offeredBy = $(this).next().text().trim();
124+
}
125+
});
126+
127+
if (!updatedOnStr || !version || !offeredBy) {
128+
throw new Error('Failed to parse the page');
129+
}
130+
131+
const updatedDate = parseDate(updatedOnStr, ...keywords.updatedOnFormat[lang]);
132+
133+
const whatsNew = $('div[itemprop=description]').html();
134+
135+
const feedContent = `
136+
<h2>${keywords.whatsNew[lang]}</h2>
137+
<p>${whatsNew ?? 'No release notes'}</p>
138+
`;
139+
140+
return {
141+
title: appName + ' - Google Play',
142+
link,
143+
image: appImage,
144+
item: [
145+
{
146+
title: formatVersion(version, updatedDate),
147+
description: feedContent,
148+
link,
149+
pubDate: updatedDate,
150+
guid: formatGuid(version, updatedDate),
151+
author: offeredBy,
152+
},
153+
],
154+
};
155+
} finally {
156+
await browser?.close();
157+
}
158+
}
159+
160+
function formatVersion(version: string, updatedDate: Date) {
161+
// some apps show version as "Varies with device"
162+
// https://play.google.com/store/apps/details?id=com.adobe.reader&hl=en-us
163+
const isVersion = /^\d/.test(version);
164+
return isVersion ? version : updatedDate.toISOString().slice(0, 10);
165+
}
166+
167+
function formatGuid(version: string, updatedDate: Date) {
168+
return updatedDate.getTime().toString() + '-' + version;
169+
}

0 commit comments

Comments
 (0)