feat(route): add HelloGitHub (#11000)

* feat(route): add HelloGitHub

* fix typo

* fix: wrong templates

* fix typo
This commit is contained in:
Ethan Shen
2022-10-05 22:30:49 +08:00
committed by GitHub
parent ab884a28be
commit a6b0803951
13 changed files with 568 additions and 153 deletions

View File

@@ -397,23 +397,58 @@ GitHub 官方也提供了一些 RSS:
## HelloGitHub ## HelloGitHub
### 文章列表 ### 热门
<Route author="moke8" example="/hellogithub/article" path="/hellogithub/article"/> <Route author="nczitzk" example="/hellogithub/hot" path="/hellogithub/hot/:id?" :paramsDesc="['标签 id可在对应标签页 URL 中找到,默认为全部标签']">
### 编程语言排行榜 以下为部分标签:
<Route author="moke8" example="/hellogithub/ranking" path="/hellogithub/ranking/:type?" :paramsDesc="['分类,见下表']"> | id | 标签 |
| ---------- | ------ |
| Z8PipJsHCX | Python |
| YQHn0gERoi | C |
| WTbsu5GAfC | CLI |
| juBLV86qa5 | 机器学习 |
| D4JBAUo967 | Rust |
| dFA60uKLgr | GUI |
| 0LByh3tjUO | 教程 |
| 4lpGK0sUyk | Web 应用 |
| yrZkGsUC9M | C++ |
| mbP20HIEYD | Ruby |
| 编程语言排行 | 数据库排行 | 服务端语言排行 | </Route>
| ------ | ----- | --------- |
| tiobe | db | webserver | ### 最近
<Route author="nczitzk" example="/hellogithub/last" path="/hellogithub/last/:id?" :paramsDesc="['标签 id可在对应标签页 URL 中找到,默认为全部标签']">
部分标签见上表
</Route>
### 文章
<Route author="moke8 nczitzk" example="/hellogithub/article" path="/hellogithub/article/:sort?/:id?" :paramsDesc="['排序方式,见下表,默认为 `hot`,即热门', '标签 id可在对应标签页 URL 中找到,默认为全部标签']">
| 热门 | 最近 |
| --- | ---- |
| hot | last |
</Route>
### 排行榜
<Route author="moke8 nczitzk" example="/hellogithub/report" path="/hellogithub/report/:type?" :paramsDesc="['分类,见下表,默认为编程语言排行榜']">
| 编程语言 | 服务器 | 数据库 |
| ----- | -------- | ---------- |
| tiobe | netcraft | db-engines |
</Route> </Route>
### 月刊 ### 月刊
<Route author="moke8" example="/hellogithub/month" path="/hellogithub/month"/> <Route author="moke8 nczitzk" example="/hellogithub/volume" path="/hellogithub/volume"/>
## Hex-Rays ## Hex-Rays

View File

@@ -1,47 +0,0 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
const timezone = require('@/utils/timezone');
module.exports = async (ctx) => {
const rootUrl = 'https://hellogithub.com';
const currentUrl = 'https://hellogithub.com/article';
const response = await got({
method: 'get',
url: currentUrl,
});
const $ = cheerio.load(response.data);
const list = $('.content .post')
.slice(0, 10)
.map((_, item) => {
const a = $(item).find('a.post-title').eq(0).eq(0);
const link = rootUrl + a.attr('href');
return {
title: a.text(),
link,
description: $(item).find('.post-description')[0].children[0].data,
pubDate: timezone(parseDate($(item).find('.post-meta').text(), 'YYYY-MM-DD HH:mm:ss'), +8),
};
})
.get();
const items = await Promise.all(
list.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const detailResponse = await got({
method: 'get',
url: item.link,
});
const content = cheerio.load(detailResponse.data);
item.description = content('.markdown-body').html();
return item;
})
)
);
ctx.state.data = {
title: 'HelloGitHub - Article',
link: currentUrl,
item: items,
};
};

102
lib/v2/hellogithub/index.js Normal file
View File

@@ -0,0 +1,102 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const sorts = {
hot: '热门',
last: '最近',
};
module.exports = async (ctx) => {
const sort = ctx.params.sort ?? 'hot';
const id = ctx.params.id ?? '';
const limit = ctx.query.limit ? parseInt(ctx.query.limit) : 20;
const rootUrl = 'https://hellogithub.com';
const apiRootUrl = 'https://api.hellogithub.com';
const currentUrl = `${rootUrl}/?sort_by=${sort}${id ? `&tid=${id}` : ''}`;
const apiUrl = `${apiRootUrl}/v1/?sort_by=${sort}${id ? `&tid=${id}` : ''}&page=1`;
const response = await got({
method: 'get',
url: apiUrl,
});
let buildId, tag;
if (id) {
const tagUrl = `${rootUrl}/tags/${id}`;
const tagResponse = await got({
method: 'get',
url: tagUrl,
});
const $ = cheerio.load(tagResponse.data);
tag = $('meta[property="og:title"]').attr('content').split(' ').pop();
buildId = tagResponse.data.match(/"buildId":"(.*?)",/)[1];
}
if (!buildId) {
const buildResponse = await got({
method: 'get',
url: rootUrl,
});
buildId = buildResponse.data.match(/"buildId":"(.*?)",/)[1];
}
let items = response.data.data.slice(0, limit).map((item) => ({
guid: item.item_id,
title: item.title,
author: item.author,
link: `${rootUrl}/repository/${item.item_id}`,
description: item.description,
pubDate: parseDate(item.updated_at),
}));
items = await Promise.all(
items.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const detailUrl = `${rootUrl}/_next/data/${buildId}/repository/${item.guid}.json`;
const detailResponse = await got({
method: 'get',
url: detailUrl,
});
const data = detailResponse.data.pageProps.repo;
item.title = `${data.name}: ${data.title}`;
item.category = [`No.${data.volume_name}`, ...data.tags.map((t) => t.name)];
item.description = art(path.join(__dirname, 'templates/description.art'), {
name: data.full_name,
description: data.description,
summary: data.summary,
image: data.image_url,
stars: data.stars ?? data.stars_str,
isChinese: data.has_chinese,
language: data.primary_lang,
isActive: data.is_active,
license: data.license,
isOrganization: data.is_org,
forks: data.forks,
openIssues: data.open_issues,
subscribers: data.subscribers,
homepage: data.homepage,
url: data.url,
});
return item;
})
)
);
ctx.state.data = {
title: `HelloGithub - ${sorts[sort]}${tag || ''}项目`,
link: currentUrl,
item: items,
};
};

View File

@@ -1,5 +1,9 @@
module.exports = { module.exports = {
'/article': ['moke8'], '/article/:sort?/:id?': ['moke8', 'nczitzk'],
'/ranking/:type?': ['moke8'], '/hot/:id?': ['nczitzk'],
'/month': ['moke8'], '/last/:id?': ['nczitzk'],
'/month': ['moke8', 'nczitzk'],
'/ranking/:type?': ['moke8', 'nczitzk'],
'/report/:type?': ['nczitzk'],
'/volume': ['nczitzk'],
}; };

View File

@@ -1,42 +0,0 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
module.exports = async (ctx) => {
const rootUrl = 'https://hellogithub.com';
const currentUrl = 'https://hellogithub.com/article';
const response = await got({
method: 'get',
url: rootUrl,
});
const $ = cheerio.load(response.data);
let count = $('.pricing-table-price').eq(0).text();
count = count.replace(/[^0-9]/gi, '');
let start = count - 10;
const PromiseArr = [];
for (start; start <= count; start++) {
PromiseArr.push(
new Promise((resolve) => {
const link = `https://hellogithub.com/periodical/volume/${start}/`;
const issueNum = start;
return ctx.cache.tryGet(link, async () => {
const detailResponse = await got({
method: 'get',
url: link,
});
const $ = cheerio.load(detailResponse.data);
return resolve({
title: '第' + issueNum + '期',
link,
description: $('.content').html(),
});
});
})
);
}
const items = await Promise.all(PromiseArr);
ctx.state.data = {
title: 'HelloGitHub - Article',
link: currentUrl,
item: items,
};
};

View File

@@ -3,22 +3,46 @@ module.exports = {
_name: 'HelloGitHub', _name: 'HelloGitHub',
'.': [ '.': [
{ {
title: '文章列表', title: '热门',
docs: 'https://docs.rsshub.app/programming.html#hellogithub', docs: 'https://docs.rsshub.app/programming.html#hellogithub-re-men',
source: ['/article', '/article/?url=/periodical/volume/'], source: ['/'],
target: '/hellogithub/article', target: (params, url) => {
const sort = new URL(url).searchParams.get('sort_by');
const id = new URL(url).searchParams.get('tid');
return `/hellogithub${sort ? `/sort` : ''}${id ? `/id` : ''}`;
},
}, },
{ {
title: '编程语言排行榜', title: '最近',
docs: 'https://docs.rsshub.app/programming.html#hellogithub', docs: 'https://docs.rsshub.app/programming.html#hellogithub-zui-jin',
source: '/report/:type/?url=/periodical/volume/', source: ['/'],
target: '/hellogithub/ranking/:type', target: (params, url) => {
const sort = new URL(url).searchParams.get('sort_by');
const id = new URL(url).searchParams.get('tid');
return `/hellogithub${sort ? `/sort` : ''}${id ? `/id` : ''}`;
},
},
{
title: '文章',
docs: 'https://docs.rsshub.app/programming.html#hellogithub-wen-zhang',
source: ['/'],
target: (params, url) => {
const sort = new URL(url).searchParams.get('sort_by');
const id = new URL(url).searchParams.get('tid');
return `/hellogithub/article${sort ? `/sort` : ''}${id ? `/id` : ''}`;
},
},
{
title: '排行榜',
docs: 'https://docs.rsshub.app/programming.html#hellogithub-pai-hang-bang',
source: ['/report/:type', '/'],
target: '/hellogithub/report/:type',
}, },
{ {
title: '月刊', title: '月刊',
docs: 'https://docs.rsshub.app/programming.html#hellogithub', docs: 'https://docs.rsshub.app/programming.html#hellogithub-yue-kan',
source: '/periodical/volume/', source: ['/periodical/volume/:id', '/'],
target: '/hellogithub/month', target: '/hellogithub/volume',
}, },
], ],
}, },

View File

@@ -1,39 +0,0 @@
const got = require('@/utils/got');
const cheerio = require('cheerio');
module.exports = async (ctx) => {
let type;
switch (ctx.params.type) {
case 'db':
type = 'db-engines';
break;
case 'webserver':
type = 'netcraft';
break;
default:
type = 'tiobe';
break;
}
const rootUrl = 'https://hellogithub.com/report/' + type;
ctx.state.data = {
title: 'HelloGitHub - ranking',
link: rootUrl,
item: await ctx.cache.tryGet(rootUrl, async () => {
const response = await got({
method: 'get',
url: rootUrl,
});
const $ = cheerio.load(response.data);
const content = $('.content');
const title = $('.header').find('h1');
return [
{
title: title.text(),
link: rootUrl,
description: content.html(),
},
];
}),
};
};

View File

@@ -0,0 +1,56 @@
const got = require('@/utils/got');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const types = {
tiobe: '编程语言',
netcraft: '服务器',
'db-engines': '数据库',
};
module.exports = async (ctx) => {
let type = ctx.params.type ?? 'tiobe';
type = type === 'webserver' ? 'netcraft' : type === 'db' ? 'db-engines' : type;
const rootUrl = 'https://hellogithub.com';
const currentUrl = `${rootUrl}/report/${type}`;
const buildResponse = await got({
method: 'get',
url: rootUrl,
});
const buildId = buildResponse.data.match(/"buildId":"(.*?)",/)[1];
const apiUrl = `${rootUrl}/_next/data/${buildId}/report/${type}.json`;
const response = await got({
method: 'get',
url: apiUrl,
});
const data = response.data.pageProps;
const items = [
{
guid: `${type}:${data.year}${data.month}`,
title: `${data.year}${data.month}${types[type]}排行榜`,
link: currentUrl,
pubDate: parseDate(`${data.year}-${data.month}`, 'YYYY-M'),
description: art(path.join(__dirname, 'templates/report.art'), {
tiobe_list: type === 'tiobe' ? data.list : undefined,
active_list: data.active_list,
all_list: data.all_list,
db_list: type === 'db-engines' ? data.list : undefined,
}),
},
];
ctx.state.data = {
title: `HelloGitHub - ${types[type]}排行榜`,
link: currentUrl,
item: items,
};
};

View File

@@ -1,5 +1,8 @@
module.exports = (router) => { module.exports = (router) => {
router.get('/article', require('./article')); router.get('/article/:sort?/:id?', require('./index'));
router.get('/month', require('./month')); router.get('/month', require('./volume'));
router.get('/ranking/:type?', require('./ranking')); router.get('/ranking/:type?', require('./report'));
router.get('/report/:type?', require('./report'));
router.get('/volume', require('./volume'));
router.get('/:sort?/:id?', require('./index'));
}; };

View File

@@ -0,0 +1,99 @@
{{ if image }}
<figure>
<img src="{{ image }}">
</figure>
{{ /if }}
<table>
<tbody>
{{ if homepage }}
<tr>
<th>Homepage</th>
<td>{{ homepage }}</td>
</tr>
{{ /if }}
{{ if name && url }}
<tr>
<th>GitHub Repo</th>
<td><a href="{{ url }}">{{ name }}</a></td>
</tr>
{{ /if }}
{{ if description }}
<tr>
<th>Description</th>
<td>{{ description }}</td>
</tr>
{{ /if }}
{{ if summary }}
<tr>
<th>Summary</th>
<td>{{ summary }}</td>
</tr>
{{ /if }}
{{ if stars }}
<tr>
<th>Stars</th>
<td>{{ stars }}</td>
</tr>
{{ /if }}
{{ if forks }}
<tr>
<th>Forks</th>
<td>{{ forks }}</td>
</tr>
{{ /if }}
{{ if subscribers }}
<tr>
<th>Subscribers</th>
<td>{{ subscribers }}</td>
</tr>
{{ /if }}
{{ if language }}
<tr>
<th>Language</th>
<td>{{ language }}</td>
</tr>
{{ /if }}
{{ if license }}
<tr>
<th>License</th>
<td>{{ license }}</td>
</tr>
{{ /if }}
<tr>
<th>Is in Chinese</th>
<td>
{{ if isChinese }}
Yes
{{ else }}
No
{{ /if }}
</td>
</tr>
<tr>
<th>Is Organization</th>
<td>
{{ if isOrganization }}
Yes
{{ else }}
No
{{ /if }}
</td>
</tr>
<tr>
<th>Is Active</th>
<td>
{{ if isActive }}
Yes
{{ else }}
No
{{ /if }}
</td>
</tr>
{{ if openIssues }}
<tr>
<th>Open Issues</th>
<td>{{ openIssues }}</td>
</tr>
{{ /if }}
</tbody>
</table>

View File

@@ -0,0 +1,118 @@
{{ if tiobe_list }}
<table>
<tbody>
<tr>
<th>排名</th>
<th>编程语言</th>
<th>流行度</th>
<th>对比上月</th>
<th>年度明星语言</th>
</tr>
{{ each tiobe_list l }}
<tr>
<td>{{ l.position }}</td>
<td>{{ l.name }}</td>
<td>{{ l.rating }}</td>
<td>
{{ if l.change }}
{{ l.change }}
{{ else }}
新上榜
{{ /if }}
</td>
<td>{{ l.star }}</td>
</tr>
{{ /each }}
</tbody>
</table>
{{ /if }}
{{ if all_list }}
<h1>市场份额排名</h1>
<table>
<tbody>
<tr>
<th>排名</th>
<th>服务器</th>
<th>占比</th>
<th>对比上月</th>
<th>总数</th>
</tr>
{{ each all_list l }}
<tr>
<td>{{ l.position }}</td>
<td>{{ l.name }}</td>
<td>{{ l.rating }}</td>
<td>
{{ if l.change }}
{{ l.change }}
{{ else }}
新上榜
{{ /if }}
</td>
<td>{{ l.total }}</td>
</tr>
{{ /each }}
</tbody>
</table>
<br>
{{ /if }}
{{ if active_list }}
<h1>活跃网站排名</h1>
<table>
<tbody>
<tr>
<th>排名</th>
<th>服务器</th>
<th>占比</th>
<th>对比上月</th>
<th>总数</th>
</tr>
{{ each active_list l }}
<tr>
<td>{{ l.position }}</td>
<td>{{ l.name }}</td>
<td>{{ l.rating }}</td>
<td>
{{ if l.change }}
{{ l.change }}
{{ else }}
新上榜
{{ /if }}
</td>
<td>{{ l.total }}</td>
</tr>
{{ /each }}
</tbody>
</table>
{{ /if }}
{{ if db_list }}
<table>
<tbody>
<tr>
<th>排名</th>
<th>数据库</th>
<th>分数</th>
<th>对比上月</th>
<th>类型</th>
</tr>
{{ each db_list l }}
<tr>
<td>{{ l.position }}</td>
<td>{{ l.name }}</td>
<td>{{ l.rating }}</td>
<td>
{{ if l.change }}
{{ l.change }}
{{ else }}
新上榜
{{ /if }}
</td>
<td>{{ l.db_model }}</td>
</tr>
{{ /each }}
</tbody>
</table>
{{ /if }}

View File

@@ -0,0 +1,36 @@
{{ if data }}
{{ each data d }}
<div>
<h1>{{ d.category_name }}</h1>
{{ each d.items item }}
<div>
<h2>
<a href="{{ item.github_url }}">{{ item.name }}</a>
</h2>
<table>
<tbody>
<tr>
<th>Stars</th>
<td>{{ item.stars }}</td>
</tr>
<tr>
<th>Forks</th>
<td>{{ item.forks }}</td>
</tr>
<tr>
<th>Watch</th>
<td>{{ item.watch }}</td>
</tr>
</tbody>
</table>
<p>{{@ item.description | render }}</p>
{{ if item.image_url }}
<figure>
<img src="{{ item.image_url }}">
</figure>
{{ /if }}
</div>
{{ /each }}
</div>
{{ /each }}
{{ /if }}

View File

@@ -0,0 +1,66 @@
const got = require('@/utils/got');
const { parseDate } = require('@/utils/parse-date');
const { art } = require('@/utils/render');
const path = require('path');
const md = require('markdown-it')({
html: true,
});
art.defaults.imports.render = function (string) {
return md.render(string);
};
module.exports = async (ctx) => {
const limit = ctx.query.limit ? parseInt(ctx.query.limit) : 30;
const rootUrl = 'https://hellogithub.com';
const apiRootUrl = 'https://api.hellogithub.com';
const currentUrl = `${rootUrl}/periodical/volume/`;
const apiUrl = `${apiRootUrl}/v1/volume/all/`;
const buildResponse = await got({
method: 'get',
url: rootUrl,
});
const buildId = buildResponse.data.match(/"buildId":"(.*?)",/)[1];
const response = await got({
method: 'get',
url: apiUrl,
});
let items = response.data.data.slice(0, limit).map((item) => ({
guid: item.num,
title: `No.${item.num}`,
link: `${rootUrl}/periodical/volume/${item.guid}`,
}));
items = await Promise.all(
items.map((item) =>
ctx.cache.tryGet(item.link, async () => {
const detailUrl = `${rootUrl}/_next/data/${buildId}/periodical/volume/${item.guid}.json`;
const detailResponse = await got({
method: 'get',
url: detailUrl,
});
const data = detailResponse.data;
item.pubDate = parseDate(data.pageProps.volume.publish_at);
item.description = art(path.join(__dirname, 'templates/volume.art'), {
data: data.pageProps.volume.data,
});
return item;
})
)
);
ctx.state.data = {
title: 'HelloGithub - 月刊',
link: currentUrl,
item: items,
};
};