diff --git a/docs/en/install/README.md b/docs/en/install/README.md
index 2f58df7263dbd9..7e6c5192d659b7 100644
--- a/docs/en/install/README.md
+++ b/docs/en/install/README.md
@@ -677,9 +677,10 @@ See docs of the specified route and `lib/config.js` for detailed information.
- Instagram:
- - `IG_USERNAME`: Your Instagram username
- - `IG_PASSWORD`: Your Instagram password
- - `IG_PROXY`: Proxy URL for Instagram
+ - `IG_USERNAME`: Your Instagram username (Private API only)
+ - `IG_PASSWORD`: Your Instagram password (Private API only)
+ - `IG_PROXY`: Proxy URL for Instagram (Private API only, optional)
+ - `IG_COOKIE`: Your Instagram cookie (Cookie only)
Warning: Two Factor Authentication is **not** supported.
diff --git a/docs/en/program-update.md b/docs/en/program-update.md
index e206db39589665..a85083b2524104 100644
--- a/docs/en/program-update.md
+++ b/docs/en/program-update.md
@@ -22,11 +22,11 @@ pageClass: routes
-## Apkpure
+## APKPure
### Versions
-
+
## App Center
@@ -116,7 +116,7 @@ Language
### BlueStacks 5 Release Notes
-
+
## Brave
diff --git a/docs/en/social-media.md b/docs/en/social-media.md
index 33d0ebf8f5fd08..c7f80753eb2fbf 100644
--- a/docs/en/social-media.md
+++ b/docs/en/social-media.md
@@ -148,15 +148,15 @@ Type
::: warning
-Due to Instagram API restrictions, you have to setup your credentials on the server. See deployment guide for more.
+Due to Instagram Private API restrictions, you have to setup your credentials on the server. 2FA is not supported. See [deployment guide](https://docs.rsshub.app/en/install/) for more.
-If you don't want to setup credentials, use Picuki.
+If you don't want to setup credentials, you can use [Picuki](#picuki).
:::
-### User Profile / Hashtag
+### User Profile / Hashtag - Private API
-
+
| User timeline | Hashtag |
| ---------- | ---- |
@@ -168,6 +168,10 @@ It's highly recommended to deploy with Redis cache enabled.
+### User Profile / Hashtag - Cookie
+
+
+
## Lofter
### User
diff --git a/docs/install/README.md b/docs/install/README.md
index ced5ffe5057efe..f8c475196c6efa 100644
--- a/docs/install/README.md
+++ b/docs/install/README.md
@@ -712,9 +712,10 @@ RSSHub 支持使用访问密钥 / 码,白名单和黑名单三种方式进行
- Instagram:
- - `IG_USERNAME`: Instagram 用户名。
- - `IG_PASSWORD`: Instagram 密码。
- - `IG_PROXY`: Instagram 代理 URL。
+ - `IG_USERNAME`: Instagram 用户名(仅 Private API)
+ - `IG_PASSWORD`: Instagram 密码(仅 Private API)
+ - `IG_PROXY`: Instagram 代理 URL(仅 Private API,可选)
+ - `IG_COOKIE`: Instagram 登录后的 Cookie(仅 Cookie)
注意,暂**不支持**两步验证。
diff --git a/docs/multimedia.md b/docs/multimedia.md
index 417cef71af6b7c..46d4e85edd0011 100644
--- a/docs/multimedia.md
+++ b/docs/multimedia.md
@@ -1561,19 +1561,19 @@ JavDB 有多个备用域名,本路由默认使用永久域名
+
**原创 BT 电影**
-| 每日合集 | 国产原创 | 亚洲无码原创 | 亚洲有码原创 | 高清中文字幕 | 三级写真 | 亚洲名站有码 | VR 系列 | 欧美无码 | 动漫原创 | AI 换脸电影 | 原档收藏 WMV | 综合讨论区 |
-| ---- | ---- | ------ | ------ | ------ | ---- | ------ | ----- | ---- | ---- | ------- | -------- | ----- |
-| mrhj | gcyc | yzwmyc | yzymyc | gqzwzm | sjxz | yzmzym | vr | omwm | dmyc | ai | ydsc | zhtlq |
+| 国产原创 | 亚洲无码原创 | 亚洲有码原创 | 高清中文字幕 | 三级写真 | VR 视频 | 素人有码 | 欧美无码 | 韩国主播 | 动漫原创 | 综合讨论 |
+| ---- | ------ | ------ | ------ | ---- | ----- | ---- | ---- | ---- | ---- | ---- |
+| gcyc | yzwmyc | yzymyc | gqzwzm | sjxz | vr | srym | omwm | hgzb | dmyc | zhtl |
**色花图片**
-| 华人性爱自拍 | 华人街拍区 | 亚洲性爱 | 欧美性爱 | 卡通动漫 |
-| ------ | ----- | ---- | ---- | ---- |
-| hrxazp | hrjpq | yzxa | omxa | ktdm |
+| 原创自拍 | 转贴自拍 | 华人街拍 | 亚洲性爱 | 欧美性爱 | 卡通动漫 | 套图下载 |
+| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
+| yczp | ztzp | hrjp | yzxa | omxa | ktdm | ttxz |
diff --git a/docs/program-update.md b/docs/program-update.md
index 66ed5c4fd080e0..8074916f321b71 100644
--- a/docs/program-update.md
+++ b/docs/program-update.md
@@ -32,11 +32,11 @@ pageClass: routes
-## Apkpure
+## APKPure
-### Versions
+### 所有版本
-
+
## App Center
@@ -114,7 +114,7 @@ pageClass: routes
### BlueStacks 5 版本日誌
-
+
## Brave
diff --git a/docs/social-media.md b/docs/social-media.md
index 864003685b9960..06ea9e7cd345fc 100644
--- a/docs/social-media.md
+++ b/docs/social-media.md
@@ -488,13 +488,13 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
::: warning 注意
-由于 Instagram API 限制,必须在服务器上设置你的用户名和密码。暂不支持两步验证。步骤见部署指南。
+由于 Instagram Private API 限制,必须在服务器上设置你的用户名和密码。暂不支持两步验证。步骤见[部署指南](https://docs.rsshub.app/install/)。
-如需无登录的 feed,请用 Picuki。
+如需无登录的 feed,请用 [Picuki](#picuki)。
:::
-### 用户 / 标签
+### 用户 / 标签 - Private API
@@ -508,6 +508,10 @@ Tiny Tiny RSS 会给所有 iframe 元素添加 `sandbox="allow-scripts"` 属性
+### 用户 / 标签 - Cookie
+
+
+
## Keep
### 运动日记
diff --git a/lib/config.js b/lib/config.js
index 124ef72a023647..beed36b5e666f6 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -185,6 +185,7 @@ const calculateValue = () => {
username: envs.IG_USERNAME,
password: envs.IG_PASSWORD,
proxy: envs.IG_PROXY,
+ cookie: envs.IG_COOKIE,
},
iwara: {
cookie: envs.IWARA_COOKIE,
diff --git a/lib/router.js b/lib/router.js
index f99202a55b4e11..9b1d94e06fd601 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1088,7 +1088,7 @@ router.get('/anigamer/new_anime', lazyloadRouteHandler('./routes/anigamer/new_an
router.get('/anigamer/anime/:sn', lazyloadRouteHandler('./routes/anigamer/anime'));
// Apkpure
-router.get('/apkpure/versions/:region/:pkg', lazyloadRouteHandler('./routes/apkpure/versions'));
+// router.get('/apkpure/versions/:region/:pkg', lazyloadRouteHandler('./routes/apkpure/versions'));
// 豆瓣美女 migrated to v2
// router.get('/dbmv/:category?', lazyloadRouteHandler('./routes/dbmv/index'));
diff --git a/lib/routes/apkpure/versions.js b/lib/routes/apkpure/versions.js
deleted file mode 100644
index f108a85c6dd6b1..00000000000000
--- a/lib/routes/apkpure/versions.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const got = require('@/utils/got');
-const cheerio = require('cheerio');
-
-module.exports = async (ctx) => {
- const { region, pkg } = ctx.params;
- const link = `https://apkpure.com/${region}/${pkg}/versions`;
- const $ = await got.get(link).then((r) => cheerio.load(r.data));
- const img = new URL($('.ver-top img').attr('src'));
- img.searchParams.delete('w'); // get full resolution icon
- ctx.state.data = {
- title: $('.ver-top-h1').text(),
- description: ` ` + $('.ver-top-title>h2').text(),
- link: decodeURI(link),
- item: $('.ver li')
- .toArray()
- .map((ver) => ({
- title: $(ver).find('.ver-item-n').text(),
- description: $(ver).find('a').attr('title'),
- link: `https://apkpure.com${decodeURI($(ver).find('a').attr('href').split('?from')[0])}`,
- pubDate: new Date($(ver).find('.update-on').text().replace(/年|月/g, '-').replace('日', '')).toUTCString(),
- })),
- };
-};
diff --git a/lib/v2/apkpure/maintainer.js b/lib/v2/apkpure/maintainer.js
new file mode 100644
index 00000000000000..b95ea3b28c2939
--- /dev/null
+++ b/lib/v2/apkpure/maintainer.js
@@ -0,0 +1,3 @@
+module.exports = {
+ '/versions/:pkg/:region?': ['maple3142'],
+};
diff --git a/lib/v2/apkpure/radar.js b/lib/v2/apkpure/radar.js
new file mode 100644
index 00000000000000..18ef4d5d648617
--- /dev/null
+++ b/lib/v2/apkpure/radar.js
@@ -0,0 +1,13 @@
+module.exports = {
+ 'apkpure.com': {
+ _name: 'APKPure',
+ '.': [
+ {
+ title: '所有版本',
+ docs: 'https://docs.rsshub.app/program-update.html#apkpure',
+ source: ['/:region/:stuff/:pkg/versions', '/:stuff/:pkg/versions', '/:stuff/:pkg'],
+ target: (params) => `/apkpure/versions/${params.pkg}${params.region ? `/${params.region}` : ''}`,
+ },
+ ],
+ },
+};
diff --git a/lib/v2/apkpure/router.js b/lib/v2/apkpure/router.js
new file mode 100644
index 00000000000000..6e779c25c4eb55
--- /dev/null
+++ b/lib/v2/apkpure/router.js
@@ -0,0 +1,3 @@
+module.exports = (router) => {
+ router.get('/versions/:pkg/:region?', require('./versions'));
+};
diff --git a/lib/v2/apkpure/versions.js b/lib/v2/apkpure/versions.js
new file mode 100644
index 00000000000000..263b75d906d210
--- /dev/null
+++ b/lib/v2/apkpure/versions.js
@@ -0,0 +1,48 @@
+const cheerio = require('cheerio');
+const logger = require('@/utils/logger');
+const { parseDate } = require('@/utils/parse-date');
+
+module.exports = async (ctx) => {
+ const { pkg, region = 'en' } = ctx.params;
+ const baseUrl = 'https://apkpure.com';
+ const link = `${baseUrl}/${region}/${pkg}/versions`;
+
+ const browser = await require('@/utils/puppeteer')();
+ const page = await browser.newPage();
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ request.resourceType() === 'document' ? request.continue() : request.abort();
+ });
+ logger.debug(`Requesting ${link}`);
+ await page.goto(link, {
+ waitUntil: 'domcontentloaded',
+ });
+
+ const r = await page.evaluate(() => document.documentElement.innerHTML);
+ browser.close();
+
+ const $ = cheerio.load(r);
+ const img = new URL($('.ver-top img').attr('src'));
+ img.searchParams.delete('w'); // get full resolution icon
+
+ const items = $('.ver li')
+ .toArray()
+ .map((ver) => {
+ ver = $(ver);
+ return {
+ title: ver.find('.ver-item-n').text(),
+ description: ver.html(),
+ link: `${baseUrl}${ver.find('a').attr('href')}`,
+ pubDate: parseDate(ver.find('.update-on').text().replace(/年|月/g, '-').replace('日', '')),
+ };
+ });
+
+ ctx.state.data = {
+ title: $('.ver-top-h1').text(),
+ description: $('.ver-top-title p').text(),
+ image: img.href,
+ language: region ? region : 'en',
+ link,
+ item: items,
+ };
+};
diff --git a/lib/v2/dsndsht23/index.js b/lib/v2/dsndsht23/index.js
deleted file mode 100644
index 1aac5fe1f1d44a..00000000000000
--- a/lib/v2/dsndsht23/index.js
+++ /dev/null
@@ -1,121 +0,0 @@
-const got = require('@/utils/got');
-const cheerio = require('cheerio');
-const { parseDate } = require('@/utils/parse-date');
-
-const host = 'https://www.sehuatang.net/';
-
-const forumIdMaps = {
- mrhj: '106',
- gcyc: '2',
- yzwmyc: '36',
- yzymyc: '37',
- gqzwzm: '103',
- sjxz: '107',
- yzmzym: '104',
- vr: '102',
- omwm: '38',
- dmyc: '39',
- ai: '113',
- ydsc: '111',
- hrxazp: '98',
- hrjpq: '50',
- yzxa: '48',
- omxa: '49',
- ktdm: '117',
- zhtlq: '95',
-};
-
-module.exports = async (ctx) => {
- const subformName = ctx.params.subforumid ?? 'gqzwzm';
- const subformId = subformName in forumIdMaps ? forumIdMaps[subformName] : subformName;
- const typefilter = ctx.params.type ? `&filter=typeid&typeid=${ctx.params.type}` : '';
- const link = `${host}forum.php?mod=forumdisplay&orderby=dateline&fid=${subformId}${typefilter}`;
- const headers = {
- 'Accept-Encoding': 'gzip, deflate, br',
- 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
- };
- let c;
- const response = await got(link, {
- headers,
- hooks: {
- beforeRedirect: [
- (options, response) => {
- const cookie = response.headers['set-cookie'];
- if (cookie) {
- const cook = cookie.map((c) => c.split(';')[0]).join('; ');
- options.headers.Cookie = cook;
- c = cook;
- options.headers.Referer = response.url;
- }
- },
- ],
- },
- });
- headers.Cookie = c;
- const $ = cheerio.load(response.data);
-
- const list = $('#threadlisttableid tbody[id^=normalthread]')
- .slice(0, ctx.query.limit ? parseInt(ctx.query.limit) : 25)
- .map(function () {
- const info = {
- title: '[' + $(this).find('th em a').text() + '] ' + $(this).find('a.xst').text(),
- link: $(this).find('a.xst').attr('href'),
- date: $(this).find('td.by').find('em span span').attr('title') || $(this).find('td.by').find('em span').first().text(),
- };
- return info;
- })
- .get();
-
- const out = await Promise.all(
- list.map((info) => {
- const title = info.title;
- const date = info.date;
- const itemUrl = host + info.link;
-
- return ctx.cache.tryGet(itemUrl, async () => {
- const response = await got(itemUrl, {
- headers,
- });
-
- const $ = cheerio.load(response.data);
- const postMessage = $("td[id^='postmessage']").slice(0, 1);
- const images = $(postMessage).find('img');
- for (let k = 0; k < images.length; k++) {
- if (!$(images[k]).attr('file') || $(images[k]).attr('file') === 'undefined') {
- $(images[k]).replaceWith('');
- } else {
- $(images[k]).replaceWith(``);
- }
- }
- const description = (postMessage.html() || '抓取原帖失败').replace(/ignore_js_op/g, 'div');
-
- const single = {
- title,
- link: itemUrl,
- description,
- pubDate: date ? parseDate(date) : null,
- };
- const magnet = postMessage.find('div.blockcode li').first().text();
- const isMag = magnet.startsWith('magnet');
- const torrent = postMessage.find('a[href^=forum\\.php\\?mod\\=attachment]:not([href$=nothumb\\=yes])').attr('href');
-
- const hasEnclosureUrl = isMag || torrent !== undefined;
- if (hasEnclosureUrl) {
- const enclosureUrl = isMag ? magnet : new URL(torrent, host).href;
- const enclosure = {
- enclosure_url: enclosureUrl,
- enclosure_type: isMag ? 'application/x-bittorrent' : 'application/octet-stream',
- };
- Object.assign(single, enclosure);
- }
- return single;
- });
- })
- );
-
- ctx.state.data = {
- title: `色花堂 - ${$('#pt > div:nth-child(1) > a:last-child').text()}`,
- link,
- item: out,
- };
-};
diff --git a/lib/v2/instagram/common-utils.js b/lib/v2/instagram/common-utils.js
new file mode 100644
index 00000000000000..8ab6c32e9c10bd
--- /dev/null
+++ b/lib/v2/instagram/common-utils.js
@@ -0,0 +1,59 @@
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+
+const renderItems = (items) =>
+ items.map((item) => {
+ const { product_type } = item; // carousel_container, feed, clips, igtv
+ // Content
+ const summary = item.caption?.text ?? '';
+
+ let description = '';
+ switch (product_type) {
+ case 'carousel_container': {
+ const images = item.carousel_media.map((i) => i.image_versions2.candidates[0]);
+ description = art(path.join(__dirname, 'templates/images.art'), {
+ summary,
+ images,
+ });
+ break;
+ }
+ case 'clips':
+ case 'igtv':
+ description = art(path.join(__dirname, 'templates/video.art'), {
+ summary,
+ image: item.image_versions2.candidates[0].url,
+ video: item.video_versions[0],
+ });
+ break;
+ case 'feed': {
+ const images = [item.image_versions2.candidates[0]];
+ description = art(path.join(__dirname, 'templates/images.art'), {
+ summary,
+ images,
+ });
+ break;
+ }
+ default:
+ throw Error(`Instagram: Unhandled feed type: ${product_type}`);
+ }
+
+ // Metadata
+ const url = `https://www.instagram.com/p/${item.code}/`;
+ const pubDate = parseDate(item.taken_at, 'X');
+ const title = summary.split('\n')[0];
+
+ return {
+ title,
+ id: item.pk,
+ pubDate,
+ author: item.user.username,
+ link: url,
+ summary,
+ description,
+ };
+ });
+
+module.exports = {
+ renderItems,
+};
diff --git a/lib/v2/instagram/maintainer.js b/lib/v2/instagram/maintainer.js
index 9e31b9d5323a7d..1d19aac1817850 100644
--- a/lib/v2/instagram/maintainer.js
+++ b/lib/v2/instagram/maintainer.js
@@ -1,3 +1,4 @@
module.exports = {
'/:category/:key': ['oppilate', 'DIYgod'],
+ '/2/:category/:key': ['TonyRL'],
};
diff --git a/lib/v2/instagram/private-api/index.js b/lib/v2/instagram/private-api/index.js
index ab2b7a43f86470..bd188d57fa25be 100644
--- a/lib/v2/instagram/private-api/index.js
+++ b/lib/v2/instagram/private-api/index.js
@@ -1,9 +1,7 @@
const { ig, login } = require('./utils');
const logger = require('@/utils/logger');
const config = require('@/config').value;
-const { parseDate } = require('@/utils/parse-date');
-const { art } = require('@/utils/render');
-const path = require('path');
+const { renderItems } = require('../common-utils');
// loadContent pulls the desired user/tag/etc
async function loadContent(category, nameOrId, tryGet) {
@@ -24,7 +22,8 @@ async function loadContent(category, nameOrId, tryGet) {
}
feedDescription = userInfo.biography;
- feedLogo = userInfo.hd_profile_pic_url_info?.url ?? userInfo.profile_pic_url;
+ // exists in web api ?? exist in private api ?? exist in both
+ feedLogo = userInfo.profile_pic_url_hd ?? userInfo.hd_profile_pic_url_info?.url ?? userInfo.profile_pic_url;
const fullName = userInfo.full_name;
feedTitle = `${fullName} (@${username}) - Instagram`;
feedLink = `https://www.instagram.com/${username}`;
@@ -41,9 +40,8 @@ async function loadContent(category, nameOrId, tryGet) {
itemsRaw = await tryGet(`instagram:tags:${tag}`, () => ig.feed.tags(tag, 'recent').items(), config.cache.routeExpire, false);
break;
}
- default: {
+ default:
break;
- }
}
return {
@@ -83,62 +81,11 @@ module.exports = async (ctx) => {
throw e;
}
- const items = data.itemsRaw.map((item) => {
- const { product_type } = item; // carousel_container, feed, clips, igtv
- // Content
- const summary = item.caption?.text ?? '';
-
- let description = '';
- switch (product_type) {
- case 'carousel_container': {
- const images = item.carousel_media.map((i) => i.image_versions2.candidates[0]);
- description = art(path.join(__dirname, '../templates/images.art'), {
- summary,
- images,
- });
- break;
- }
- case 'clips':
- case 'igtv':
- description = art(path.join(__dirname, '../templates/video.art'), {
- summary,
- image: item.image_versions2.candidates[0].url,
- video: item.video_versions[0],
- });
- break;
- case 'feed': {
- const images = [item.image_versions2.candidates[0]];
- description = art(path.join(__dirname, '../templates/images.art'), {
- summary,
- images,
- });
- break;
- }
- default:
- throw Error(`Instagram: Unhandled feed type: ${product_type}`);
- }
-
- // Metadata
- const url = `https://www.instagram.com/p/${item.code}/`;
- const pubDate = parseDate(item.taken_at, 'X');
- const title = summary.split('\n')[0];
-
- return {
- title,
- id: item.pk,
- pubDate,
- author: item.user.username,
- link: url,
- summary,
- description,
- };
- });
-
ctx.state.data = {
title: data.feedTitle,
link: data.feedLink,
description: data.feedDescription,
- item: items,
+ item: renderItems(data.itemsRaw),
icon: 'https://www.instagram.com/static/images/ico/xxhdpi_launcher.png/99cf3909d459.png',
logo: data.feedLogo,
image: data.feedLogo,
diff --git a/lib/v2/instagram/router.js b/lib/v2/instagram/router.js
index eda1408aa6b0b3..9672c20cd58020 100644
--- a/lib/v2/instagram/router.js
+++ b/lib/v2/instagram/router.js
@@ -1,3 +1,4 @@
module.exports = (router) => {
router.get('/:category/:key', require('./private-api/index'));
+ router.get('/2/:category/:key', require('./web-api/index'));
};
diff --git a/lib/v2/instagram/web-api/index.js b/lib/v2/instagram/web-api/index.js
new file mode 100644
index 00000000000000..658b8162a29457
--- /dev/null
+++ b/lib/v2/instagram/web-api/index.js
@@ -0,0 +1,70 @@
+const { CookieJar } = require('tough-cookie');
+const config = require('@/config').value;
+const { renderItems } = require('../common-utils');
+const { baseUrl, COOKIE_URL, getUserInfo, getUserFeedItems, getTagsFeedItems } = require('./utils');
+
+module.exports = async (ctx) => {
+ if (!config.instagram || !config.instagram.cookie) {
+ throw Error('Instagram RSS is disabled due to the lack of relevant config');
+ }
+ const availableCategories = ['user', 'tags'];
+ const { category, key } = ctx.params;
+ const { cookie } = config.instagram;
+ if (!availableCategories.includes(category)) {
+ throw Error('Such feed is not supported.');
+ }
+
+ let cookieJar = await ctx.cache.get('instagram:cookieJar');
+ const cacheMiss = !cookieJar;
+ if (cacheMiss) {
+ cookieJar = new CookieJar();
+ for await (const c of cookie.split('; ')) {
+ await cookieJar.setCookie(c, COOKIE_URL);
+ }
+ } else {
+ cookieJar = CookieJar.fromJSON(cookieJar);
+ }
+
+ let feedTitle, feedLink, feedDescription, feedLogo;
+ let items;
+ switch (category) {
+ case 'user': {
+ const userInfo = await getUserInfo(key, cookieJar, ctx.cache);
+
+ // User feed metadata
+ const { biography, full_name, id, username } = userInfo;
+ feedTitle = `${full_name} (@${username}) - Instagram`;
+ feedDescription = biography;
+ // exists in web api ?? exist in private api ?? exist in both
+ feedLogo = userInfo.profile_pic_url_hd ?? userInfo.hd_profile_pic_url_info?.url ?? userInfo.profile_pic_url;
+ feedLink = `${baseUrl}/${username}`;
+
+ items = await getUserFeedItems(id, username, cookieJar, ctx.cache.tryGet);
+ break;
+ }
+ case 'tags': {
+ const tag = key;
+
+ feedTitle = `#${tag} - Instagram`;
+ feedLink = `${baseUrl}/explore/tags/${tag}`;
+
+ items = await getTagsFeedItems(tag, 'recent', cookieJar, ctx.cache.tryGet);
+ break;
+ }
+ default:
+ break;
+ }
+
+ await ctx.cache.set('instagram:cookieJar', cookieJar.toJSON(), 31536000);
+
+ ctx.state.data = {
+ title: feedTitle,
+ link: feedLink,
+ description: feedDescription,
+ item: renderItems(items),
+ icon: `${baseUrl}/static/images/ico/xxhdpi_launcher.png/99cf3909d459.png`,
+ logo: feedLogo,
+ image: feedLogo,
+ allowEmpty: true,
+ };
+};
diff --git a/lib/v2/instagram/web-api/utils.js b/lib/v2/instagram/web-api/utils.js
new file mode 100644
index 00000000000000..2bf6bc47412663
--- /dev/null
+++ b/lib/v2/instagram/web-api/utils.js
@@ -0,0 +1,99 @@
+const got = require('@/utils/got');
+const config = require('@/config').value;
+
+const baseUrl = 'https://www.instagram.com';
+const COOKIE_URL = 'https://instagram.com';
+let igWwwClaim;
+
+const getCSRFTokenFromJar = async (cookieJar) => {
+ const cookieString = await cookieJar.getCookieString(COOKIE_URL);
+ return cookieString.match(/csrftoken=([^;]+)/)?.[1];
+};
+
+const getHeaders = async (cookieJar) => ({
+ 'X-ASBD-ID': 198387,
+ 'X-CSRFToken': await getCSRFTokenFromJar(cookieJar),
+ 'X-IG-App-ID': 936619743392459,
+ 'X-IG-WWW-Claim': igWwwClaim,
+});
+
+const getUserInfo = async (username, cookieJar, cache) => {
+ let webProfileInfo;
+ let id = await cache.get(`instagram:getIdByUsername:${username}`);
+ let userInfoCache = await cache.get(`instagram:userInfo:${id}`);
+
+ if (!userInfoCache) {
+ const response = await got(`${baseUrl}/api/v1/users/web_profile_info/`, {
+ cookieJar,
+ headers: await getHeaders(cookieJar),
+ searchParams: {
+ username,
+ },
+ });
+ if (response.url.includes('/accounts/login/')) {
+ throw Error('Invalid cookie');
+ }
+ igWwwClaim = response.headers['x-ig-set-www-claim'] || igWwwClaim;
+
+ webProfileInfo = response.data.data.user;
+ id = webProfileInfo.id;
+
+ await cache.set(`instagram:getIdByUsername:${username}`, id, 31536000); // 1 year since it will never change
+ await cache.set(`instagram:userInfo:${id}`, webProfileInfo);
+ }
+
+ userInfoCache = typeof userInfoCache === 'string' ? JSON.parse(userInfoCache) : userInfoCache;
+
+ return userInfoCache || webProfileInfo;
+};
+
+const getUserFeedItems = (id, username, cookieJar, tryGet) =>
+ tryGet(
+ `instagram:feed:${id}`,
+ async () => {
+ const response = await got(`${baseUrl}/api/v1/feed/user/${username}/username/`, {
+ cookieJar,
+ headers: await getHeaders(cookieJar),
+ searchParams: {
+ count: 30,
+ },
+ });
+ // 401 Unauthorized if cookie does not match with IP
+ igWwwClaim = response.headers['x-ig-set-www-claim'] || igWwwClaim;
+
+ return response.data.items;
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+const getTagsFeedItems = (tag, tab, cookieJar, tryGet) =>
+ tryGet(
+ `instagram:tags:${tag}`,
+ async () => {
+ const response = await got(`${baseUrl}/api/v1/tags/web_info/`, {
+ // cookieJar, cookieJar is behaving weirdly here, so we use cookie header instead
+ headers: {
+ cookie: await cookieJar.getCookieString(COOKIE_URL),
+ ...(await getHeaders(cookieJar)),
+ },
+ searchParams: {
+ tag_name: tag,
+ },
+ });
+ // Looks like cookie IP check is not applied to tags
+ igWwwClaim = response.headers['x-ig-set-www-claim'] || igWwwClaim;
+
+ return response.data.data[tab].sections.flatMap((section) => section.layout_content.medias.map((media) => media.media));
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+module.exports = {
+ baseUrl,
+ COOKIE_URL,
+ getUserInfo,
+ getUserFeedItems,
+ getTagsFeedItems,
+};
diff --git a/lib/v2/sehuatang/index.js b/lib/v2/sehuatang/index.js
new file mode 100644
index 00000000000000..06451d87b3daae
--- /dev/null
+++ b/lib/v2/sehuatang/index.js
@@ -0,0 +1,129 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { parseDate } = require('@/utils/parse-date');
+const timezone = require('@/utils/timezone');
+const { CookieJar } = require('tough-cookie');
+const cookieJar = new CookieJar();
+
+const host = 'https://www.sehuatang.net/';
+
+const forumIdMaps = {
+ // 原创 BT 电影
+ gcyc: '2', // 国产原创
+ yzwmyc: '36', // 亚洲无码原创
+ yzymyc: '37', // 亚洲有码原创
+ gqzwzm: '103', // 高清中文字幕
+ sjxz: '107', // 三级写真
+ vr: '160', // VR 视频
+ srym: '104', // 素人有码
+ omwm: '38', // 欧美无码
+ '4k': '151', // 4K 原版
+ hgzb: '152', // 韩国主播
+ dmyc: '39', // 动漫原创
+ // 色花图片
+ yczp: '155', // 原创自拍
+ ztzp: '125', // 转贴自拍
+ hrjp: '50', // 华人街拍
+ yzxa: '48', // 亚洲性爱
+ omxa: '49', // 欧美性爱
+ ktdm: '117', // 卡通动漫
+ ttxz: '165', // 套图下载
+
+ zhtl: '95', // 综合讨论
+ // no longer updated/available
+ mrhj: '106', // 每日合集
+ ai: '113', // AI 换脸电影
+ ydsc: '111', // 原档收藏 WMV
+ hrxazp: '98', // 华人性爱自拍
+};
+
+module.exports = async (ctx) => {
+ const subformName = ctx.params.subforumid ?? 'gqzwzm';
+ const subformId = subformName in forumIdMaps ? forumIdMaps[subformName] : subformName;
+ const { type } = ctx.params;
+ const typefilter = type ? `&filter=typeid&typeid=${type}` : '';
+ const link = `${host}forum.php?mod=forumdisplay&orderby=dateline&fid=${subformId}${typefilter}`;
+ const headers = {
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
+ };
+ const response = await got(link, {
+ cookieJar,
+ headers,
+ });
+ const $ = cheerio.load(response.data);
+
+ const list = $('#threadlisttableid tbody[id^=normalthread]')
+ .slice(0, ctx.query.limit ? parseInt(ctx.query.limit) : 25)
+ .toArray()
+ .map((item) => {
+ item = $(item);
+ const hasCategory = item.find('th em a').length;
+ return {
+ title: `${hasCategory ? `[${item.find('th em a').text()}]` : ''} ${item.find('a.xst').text()}`,
+ link: host + item.find('a.xst').attr('href'),
+ pubDate: parseDate(item.find('td.by').find('em span span').attr('title')),
+ author: item.find('td.by cite a').first().text(),
+ };
+ });
+
+ const out = await Promise.all(
+ list.map((info) =>
+ ctx.cache.tryGet(info.link, async () => {
+ const response = await got(info.link, {
+ cookieJar,
+ headers,
+ });
+
+ const $ = cheerio.load(response.data);
+ const postMessage = $("td[id^='postmessage']").slice(0, 1);
+ const images = $(postMessage).find('img');
+ for (const image of images) {
+ const file = $(image).attr('file');
+ if (!file || file === 'undefined') {
+ $(image).replaceWith('');
+ } else {
+ $(image).replaceWith($(``));
+ }
+ }
+ // if postMessage does not have any images, try to parse image url from `.pattl`
+ if (images.length === 0) {
+ const pattl = $('.pattl');
+ const pattlImages = $(pattl).find('img');
+ for (const pattlImage of pattlImages) {
+ const file = $(pattlImage).attr('file');
+ if (!file || file === 'undefined') {
+ $(pattlImage).replaceWith('');
+ } else {
+ $(pattlImage).replaceWith($(``));
+ }
+ }
+ postMessage.append($(pattl));
+ }
+ $('em[onclick]').remove();
+
+ info.description = (postMessage.html() || '抓取原帖失败').replace(/ignore_js_op/g, 'div');
+ info.pubDate = timezone(parseDate($('.authi em span').attr('title')), 8);
+
+ const magnet = postMessage.find('div.blockcode li').first().text();
+ const isMag = magnet.startsWith('magnet');
+ const torrent = postMessage.find('p.attnm a').attr('href');
+
+ const hasEnclosureUrl = isMag || torrent !== undefined;
+ if (hasEnclosureUrl) {
+ const enclosureUrl = isMag ? magnet : new URL(torrent, host).href;
+ info.enclosure_url = enclosureUrl;
+ info.enclosure_type = isMag ? 'application/x-bittorrent' : 'application/octet-stream';
+ }
+
+ return info;
+ })
+ )
+ );
+
+ ctx.state.data = {
+ title: `色花堂 - ${$('#pt > div:nth-child(1) > a:last-child').text()}`,
+ link,
+ item: out,
+ };
+};
diff --git a/lib/v2/dsndsht23/maintainer.js b/lib/v2/sehuatang/maintainer.js
similarity index 100%
rename from lib/v2/dsndsht23/maintainer.js
rename to lib/v2/sehuatang/maintainer.js
diff --git a/lib/v2/dsndsht23/radar.js b/lib/v2/sehuatang/radar.js
similarity index 81%
rename from lib/v2/dsndsht23/radar.js
rename to lib/v2/sehuatang/radar.js
index 1037947f05137a..592f8fae1903c6 100644
--- a/lib/v2/dsndsht23/radar.js
+++ b/lib/v2/sehuatang/radar.js
@@ -8,10 +8,10 @@ module.exports = {
source: ['/:category', '/'],
target: (params, url) => {
const theUrl = new URL(url);
- const matches = String(theUrl).match(/forum-(\d)+-\d+/);
+ const matches = theUrl.href.match(/forum-(\d)+-\d+/);
const fid = theUrl.searchParams.get('fid') || (matches ? matches[1] : '');
const tid = theUrl.searchParams.get('typeid');
- return `/dsndsht23${fid ? `/${fid}` : ''}${tid ? `/${tid}` : ''}`;
+ return `/sehuatang${fid ? `/${fid}` : ''}${tid ? `/${tid}` : ''}`;
},
},
],
diff --git a/lib/v2/dsndsht23/router.js b/lib/v2/sehuatang/router.js
similarity index 100%
rename from lib/v2/dsndsht23/router.js
rename to lib/v2/sehuatang/router.js