diff --git a/docs/en/programming.md b/docs/en/programming.md index b7294ef3fd..f3c78fdf90 100644 --- a/docs/en/programming.md +++ b/docs/en/programming.md @@ -10,6 +10,50 @@ pageClass: routes +## AtCoder + +### Present Contests + + + +Status + +| Active Contests | Upcoming Contests | Recent Contests | +| --------------- | ----------------- | --------------- | +| active | upcoming | recent | + + + +### Contests Archive + + + +Rated Range + +| ABC Class (Rated for ~1999) | ARC Class (Rated for ~2799) | AGC Class (Rated for ~9999) | +| --------------------------- | --------------------------- | --------------------------- | +| 1 | 2 | 3 | + +Category + +| All | AtCoder Typical Contest | PAST Archive | Unofficial(unrated) | +| --- | ----------------------- | ------------ | ------------------- | +| 0 | 6 | 50 | 101 | + +| JOI Archive | Sponsored Tournament | Sponsored Parallel(rated) | +| ----------- | -------------------- | ------------------------- | +| 200 | 1000 | 1001 | + +| Sponsored Parallel(unrated) | Optimization Contest | +| --------------------------- | -------------------- | +| 1002 | 1200 | + + + +### Posts + + + ## cve.mitre.org ### Search Result diff --git a/docs/programming.md b/docs/programming.md index 1175479d5c..736c52add9 100644 --- a/docs/programming.md +++ b/docs/programming.md @@ -24,6 +24,50 @@ pageClass: routes > AlgoCasts 需要付费订阅,RSS 仅做更新提醒,不含付费内容. +## AtCoder + +### Present Contests + + + +状态 + +| Active Contests | Upcoming Contests | Recent Contests | +| --------------- | ----------------- | --------------- | +| active | upcoming | recent | + + + +### Contests Archive + + + +Rated 对象 + +| ABC Class (Rated for ~1999) | ARC Class (Rated for ~2799) | AGC Class (Rated for ~9999) | +| --------------------------- | --------------------------- | --------------------------- | +| 1 | 2 | 3 | + +分类 + +| All | AtCoder Typical Contest | PAST Archive | Unofficial(unrated) | +| --- | ----------------------- | ------------ | ------------------- | +| 0 | 6 | 50 | 101 | + +| JOI Archive | Sponsored Tournament | Sponsored Parallel(rated) | +| ----------- | -------------------- | ------------------------- | +| 200 | 1000 | 1001 | + +| Sponsored Parallel(unrated) | Optimization Contest | +| --------------------------- | -------------------- | +| 1002 | 1200 | + + + +### Posts + + + ## cve.mitre.org ### 搜索结果 diff --git a/lib/v2/atcoder/contest.js b/lib/v2/atcoder/contest.js new file mode 100644 index 0000000000..446425eba3 --- /dev/null +++ b/lib/v2/atcoder/contest.js @@ -0,0 +1,64 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const { parseDate } = require('@/utils/parse-date'); + +module.exports = async (ctx) => { + const status = ['action', 'upcoming', 'recent']; + + const language = ctx.params.language ?? 'en'; + + let rated = ctx.params.rated ?? '0'; + const category = ctx.params.category ?? '0'; + const keyword = ctx.params.keyword ?? ''; + + rated = rated === 'active' ? 'action' : rated; + const isStatus = status.includes(rated); + + const rootUrl = 'https://atcoder.jp'; + const currentUrl = `${rootUrl}/contests${isStatus ? `?lang=${language}` : `/archive?lang=${language}&ratedType=${rated}&category=${category}${keyword ? `&keyword=${keyword}` : ''}`}`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = cheerio.load(response.data); + + let items = $(isStatus ? `#contest-table-${rated}` : '.row') + .find('tr') + .slice(1, ctx.query.limit ? parseInt(ctx.query.limit) : 20) + .toArray() + .map((item) => { + item = $(item).find('td a').eq(1); + + return { + title: item.text(), + link: `${rootUrl}${item.attr('href')}?lang=${language}`, + }; + }); + + items = await Promise.all( + items.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(`.lang-${language}`).html(); + item.pubDate = parseDate(content('.fixtime-full').first().text()); + + return item; + }) + ) + ); + + ctx.state.data = { + title: String(isStatus ? `${$(`#contest-table-${rated} h3`).text()} - AtCoder` : $('title').text()), + link: currentUrl, + item: items, + allowEmpty: true, + }; +}; diff --git a/lib/v2/atcoder/maintainer.js b/lib/v2/atcoder/maintainer.js new file mode 100644 index 0000000000..9cc4dbd5ba --- /dev/null +++ b/lib/v2/atcoder/maintainer.js @@ -0,0 +1,4 @@ +module.exports = { + '/post/:language?/:keyword?': ['nczitzk'], + '/contest/:language?/:rated?/:category?/:keyword?': ['nczitzk'], +}; diff --git a/lib/v2/atcoder/post.js b/lib/v2/atcoder/post.js new file mode 100644 index 0000000000..db6732d50c --- /dev/null +++ b/lib/v2/atcoder/post.js @@ -0,0 +1,38 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); +const timezone = require('@/utils/timezone'); +const { parseDate } = require('@/utils/parse-date'); + +module.exports = async (ctx) => { + const language = ctx.params.language ?? 'en'; + const keyword = ctx.params.keyword ?? ''; + + const rootUrl = 'https://atcoder.jp'; + const currentUrl = `${rootUrl}/posts?lang=${language}${keyword ? `&keyword=${keyword}` : ''}`; + + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = cheerio.load(response.data); + + const items = $('.panel') + .toArray() + .map((item) => { + item = $(item); + + return { + title: item.find('.panel-title').text(), + description: item.find('.panel-body').html(), + link: `${rootUrl}${item.find('.panel-title a').attr('href')}`, + pubDate: timezone(parseDate(item.find('.timeago').attr('datetime')), +9), + }; + }); + + ctx.state.data = { + title: `${keyword ? `[${keyword}] - ` : ''}${$('title').text()}`, + link: currentUrl, + item: items, + }; +}; diff --git a/lib/v2/atcoder/radar.js b/lib/v2/atcoder/radar.js new file mode 100644 index 0000000000..5581fc9762 --- /dev/null +++ b/lib/v2/atcoder/radar.js @@ -0,0 +1,22 @@ +module.exports = { + 'atcoder.jp': { + _name: 'AtCoder', + '.': [ + { + title: 'Posts', + docs: 'https://docs.rsshub.app/programming.html#atcoder-posts', + source: ['/posts', '/'], + target: (params, url) => `/atcoder/post/${new URL(url).searchParams.get('lang') ?? 'en'}/${new URL(url).searchParams.get('keyword') ?? ''}`, + }, + { + title: 'Contests', + docs: 'https://docs.rsshub.app/programming.html#atcoder-contests', + source: ['/contests/archive', '/contests', ''], + target: (params, url) => + `/atcoder/content/${new URL(url).searchParams.get('lang') ?? 'en'}/${new URL(url).searchParams.get('ratedType') ?? '0'}/${new URL(url).searchParams.get('category') ?? '0'}/${ + new URL(url).searchParams.get('keyword') ?? '' + }`, + }, + ], + }, +}; diff --git a/lib/v2/atcoder/router.js b/lib/v2/atcoder/router.js new file mode 100644 index 0000000000..faa8e13386 --- /dev/null +++ b/lib/v2/atcoder/router.js @@ -0,0 +1,4 @@ +module.exports = function (router) { + router.get('/post/:language?/:keyword?', require('./post')); + router.get('/contest/:language?/:rated?/:category?/:keyword?', require('./contest')); +};