diff --git a/lib/router.js b/lib/router.js
index d1f5dda1ab6038..9ef5ac95027b31 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -93,12 +93,12 @@ router.get('/konachan.com/post/popular_recent/:period', lazyloadRouteHandler('./
router.get('/konachan.net/post/popular_recent/:period', lazyloadRouteHandler('./routes/konachan/post_popular_recent'));
// PornHub
-router.get('/pornhub/category/:caty', lazyloadRouteHandler('./routes/pornhub/category'));
-router.get('/pornhub/search/:keyword', lazyloadRouteHandler('./routes/pornhub/search'));
-router.get('/pornhub/:language?/category_url/:url?', lazyloadRouteHandler('./routes/pornhub/category_url'));
-router.get('/pornhub/:language?/users/:username', lazyloadRouteHandler('./routes/pornhub/users'));
-router.get('/pornhub/:language?/model/:username/:sort?', lazyloadRouteHandler('./routes/pornhub/model'));
-router.get('/pornhub/:language?/pornstar/:username/:sort?', lazyloadRouteHandler('./routes/pornhub/pornstar'));
+// router.get('/pornhub/category/:caty', lazyloadRouteHandler('./routes/pornhub/category'));
+// router.get('/pornhub/search/:keyword', lazyloadRouteHandler('./routes/pornhub/search'));
+// router.get('/pornhub/:language?/category_url/:url?', lazyloadRouteHandler('./routes/pornhub/category_url'));
+// router.get('/pornhub/:language?/users/:username', lazyloadRouteHandler('./routes/pornhub/users'));
+// router.get('/pornhub/:language?/model/:username/:sort?', lazyloadRouteHandler('./routes/pornhub/model'));
+// router.get('/pornhub/:language?/pornstar/:username/:sort?', lazyloadRouteHandler('./routes/pornhub/pornstar'));
// yande.re
router.get('/yande.re/post/popular_recent', lazyloadRouteHandler('./routes/yande.re/post_popular_recent'));
diff --git a/lib/routes/pornhub/category.js b/lib/routes/pornhub/category.js
deleted file mode 100644
index 5dbc5a95767566..00000000000000
--- a/lib/routes/pornhub/category.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const got = require('@/utils/got');
-
-module.exports = async (ctx) => {
- const currentUrl = `https://pornhub.com/webmasters/search?category=${ctx.params.caty}`;
- const response = await got({
- method: 'get',
- url: currentUrl,
- });
-
- const list = response.data.videos.map((item) => ({
- title: item.title,
- link: item.url,
- description: ``,
- pubDate: new Date(item.publish_date).toUTCString(),
- }));
-
- ctx.state.data = {
- title: `Pornhub - ${ctx.params.caty}`,
- link: currentUrl,
- item: list,
- };
-};
diff --git a/lib/routes/pornhub/category_url.js b/lib/routes/pornhub/category_url.js
deleted file mode 100644
index f6a4e5ce91578e..00000000000000
--- a/lib/routes/pornhub/category_url.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const got = require('@/utils/got');
-const cheerio = require('cheerio');
-const { isValidHost } = require('@/utils/valid-host');
-
-module.exports = async (ctx) => {
- const language = ctx.params.language || 'www';
- const url = ctx.params.url || 'video';
- const link = `https://${language}.pornhub.com/${url}`;
- if (!isValidHost(language)) {
- throw Error('Invalid language');
- }
-
- const response = await got.get(link);
- const $ = cheerio.load(response.data);
- const list = $('#videoCategory .videoBox');
-
- ctx.state.data = {
- title: $('title').first().text(),
- link,
- item:
- list &&
- list
- .map((_, e) => {
- e = $(e);
-
- return {
- title: e.find('span.title a').text(),
- link: `https://www.pornhub.com` + e.find('span.title a').attr('href'),
- description: ``,
- };
- })
- .get(),
- };
-};
diff --git a/lib/routes/pornhub/model.js b/lib/routes/pornhub/model.js
deleted file mode 100644
index 468db7cf1ec386..00000000000000
--- a/lib/routes/pornhub/model.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const got = require('@/utils/got');
-const cheerio = require('cheerio');
-const { isValidHost } = require('@/utils/valid-host');
-
-module.exports = async (ctx) => {
- const language = ctx.params.language || 'www';
- const username = ctx.params.username;
- const sort = ctx.params.sort || 'mr';
- const link = `https://${language}.pornhub.com/model/${username}/videos?o=${sort}`;
- if (!isValidHost(language)) {
- throw Error('Invalid language');
- }
-
- const response = await got.get(link);
- const $ = cheerio.load(response.data);
- const list = $('#mostRecentVideosSection .videoBox');
-
- ctx.state.data = {
- title: $('title').first().text(),
- link,
- item:
- list &&
- list
- .map((_, e) => {
- e = $(e);
-
- return {
- title: e.find('span.title a').text(),
- link: `https://www.pornhub.com` + e.find('span.title a').attr('href'),
- description: ``,
- };
- })
- .get(),
- };
-};
diff --git a/lib/routes/pornhub/pornstar.js b/lib/routes/pornhub/pornstar.js
deleted file mode 100644
index 771ae277674cf8..00000000000000
--- a/lib/routes/pornhub/pornstar.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const got = require('@/utils/got');
-const cheerio = require('cheerio');
-const { isValidHost } = require('@/utils/valid-host');
-
-module.exports = async (ctx) => {
- const language = ctx.params.language || 'www';
- const username = ctx.params.username;
- const sort = ctx.params.sort || 'mr';
- const link = `https://${language}.pornhub.com/pornstar/${username}/videos?o=${sort}`;
- if (!isValidHost(language)) {
- throw Error('Invalid language');
- }
-
- const response = await got.get(link);
- const $ = cheerio.load(response.data);
- const list = $('#mostRecentVideosSection .videoBox');
-
- ctx.state.data = {
- title: $('title').first().text(),
- link,
- item:
- list &&
- list
- .map((_, e) => {
- e = $(e);
-
- return {
- title: e.find('span.title a').text(),
- link: `https://www.pornhub.com` + e.find('span.title a').attr('href'),
- description: ``,
- };
- })
- .get(),
- };
-};
diff --git a/lib/routes/pornhub/search.js b/lib/routes/pornhub/search.js
deleted file mode 100644
index 4d8dc925d94088..00000000000000
--- a/lib/routes/pornhub/search.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const got = require('@/utils/got');
-
-module.exports = async (ctx) => {
- const currentUrl = `https://pornhub.com/webmasters/search?search=${ctx.params.keyword}`;
- const response = await got({
- method: 'get',
- url: currentUrl,
- });
-
- const list = response.data.videos.map((item) => ({
- title: item.title,
- link: item.url,
- description: ``,
- pubDate: new Date(item.publish_date).toUTCString(),
- }));
-
- ctx.state.data = {
- title: `Pornhub - ${ctx.params.keyword}`,
- link: currentUrl,
- item: list,
- };
-};
diff --git a/lib/routes/pornhub/users.js b/lib/routes/pornhub/users.js
deleted file mode 100644
index a632b166375ce5..00000000000000
--- a/lib/routes/pornhub/users.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const got = require('@/utils/got');
-const cheerio = require('cheerio');
-const { isValidHost } = require('@/utils/valid-host');
-
-module.exports = async (ctx) => {
- const language = ctx.params.language || 'www';
- const username = ctx.params.username;
- const link = `https://${language}.pornhub.com/users/${username}/videos`;
- if (!isValidHost(language)) {
- throw Error('Invalid language');
- }
-
- const response = await got.get(link);
- const $ = cheerio.load(response.data);
- const list = $('.videoUList .videoBox');
-
- ctx.state.data = {
- title: $('title').first().text(),
- link,
- allowEmpty: true,
- item:
- list &&
- list
- .map((_, e) => {
- e = $(e);
-
- return {
- title: e.find('span.title a').text(),
- link: `https://www.pornhub.com` + e.find('span.title a').attr('href'),
- description: ``,
- };
- })
- .get(),
- };
-};
diff --git a/lib/v2/pornhub/category.js b/lib/v2/pornhub/category.js
new file mode 100644
index 00000000000000..df09774424e338
--- /dev/null
+++ b/lib/v2/pornhub/category.js
@@ -0,0 +1,44 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { defaultDomain, renderDescription } = require('./utils');
+const config = require('@/config').value;
+
+module.exports = async (ctx) => {
+ const category = ctx.params.caty;
+
+ const categories = await ctx.cache.tryGet('pornhub:categories', async () => {
+ const { data } = await got(`${defaultDomain}/webmasters/categories`);
+ return data.categories;
+ });
+
+ const categoryId = isNaN(category) ? categories.find((item) => item.category === category)?.id : category;
+ const categoryName = isNaN(category) ? category : categories.find((item) => item.id === parseInt(category)).category;
+
+ const response = await ctx.cache.tryGet(
+ `pornhub:category:${categoryName}`,
+ async () => {
+ const { data } = await got(`${defaultDomain}/webmasters/search?category=${categoryName}`);
+ return data;
+ },
+ config.cache.routeExpire,
+ false
+ );
+
+ if (response.code) {
+ throw Error(response.message);
+ }
+
+ const list = response.videos.map((item) => ({
+ title: item.title,
+ link: item.url,
+ description: renderDescription({ thumbs: item.thumbs }),
+ pubDate: parseDate(item.publish_date),
+ category: [...new Set([...item.tags.map((t) => t.tag_name), ...item.categories.map((c) => c.category)])],
+ }));
+
+ ctx.state.data = {
+ title: `Pornhub - ${categoryName}`,
+ link: `${defaultDomain}/video?c=${categoryId}`,
+ item: list,
+ };
+};
diff --git a/lib/v2/pornhub/category_url.js b/lib/v2/pornhub/category_url.js
new file mode 100644
index 00000000000000..bdfe0fc85dca9b
--- /dev/null
+++ b/lib/v2/pornhub/category_url.js
@@ -0,0 +1,25 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
+const { headers, parseItems } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { language = 'www', url = 'video' } = ctx.params;
+ const link = `https://${language}.pornhub.com/${url}`;
+ if (!isValidHost(language)) {
+ throw Error('Invalid language');
+ }
+
+ const { data: response } = await got(link, { headers });
+ const $ = cheerio.load(response);
+ const items = $('#videoCategory .videoBox')
+ .toArray()
+ .map((e) => parseItems($(e)));
+
+ ctx.state.data = {
+ title: $('title').first().text(),
+ link,
+ language: $('html').attr('lang'),
+ item: items,
+ };
+};
diff --git a/lib/v2/pornhub/maintainer.js b/lib/v2/pornhub/maintainer.js
new file mode 100644
index 00000000000000..a3b4c367abb13e
--- /dev/null
+++ b/lib/v2/pornhub/maintainer.js
@@ -0,0 +1,8 @@
+module.exports = {
+ '/category/:caty': ['nczitzk'],
+ '/search/:keyword': ['nczitzk'],
+ '/:language?/category_url/:url?': ['I2IMk', 'queensferryme'],
+ '/:language?/model/:username/:sort?': ['I2IMk', 'queensferryme'],
+ '/:language?/pornstar/:username/:sort?': ['I2IMk', 'queensferryme'],
+ '/:language?/users/:username': ['I2IMk', 'queensferryme'],
+};
diff --git a/lib/v2/pornhub/model.js b/lib/v2/pornhub/model.js
new file mode 100644
index 00000000000000..950d0f2607c3f5
--- /dev/null
+++ b/lib/v2/pornhub/model.js
@@ -0,0 +1,29 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
+const { headers, parseItems } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { language = 'www', username, sort = '' } = ctx.params;
+ const link = `https://${language}.pornhub.com/model/${username}/videos${sort ? `?o=${sort}` : ''}`;
+ if (!isValidHost(language)) {
+ throw Error('Invalid language');
+ }
+
+ const { data: response } = await got(link, { headers });
+ const $ = cheerio.load(response);
+ const items = $('#mostRecentVideosSection .videoBox')
+ .toArray()
+ .map((e) => parseItems($(e)));
+
+ ctx.state.data = {
+ title: $('title').first().text(),
+ description: $('section.aboutMeSection').text().trim(),
+ link,
+ image: $('#coverPictureDefault').attr('src'),
+ logo: $('#getAvatar').attr('src'),
+ icon: $('#getAvatar').attr('src'),
+ language: $('html').attr('lang'),
+ item: items,
+ };
+};
diff --git a/lib/v2/pornhub/pornstar.js b/lib/v2/pornhub/pornstar.js
new file mode 100644
index 00000000000000..23fd812a32087e
--- /dev/null
+++ b/lib/v2/pornhub/pornstar.js
@@ -0,0 +1,29 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
+const { headers, parseItems } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { language = 'www', username, sort = 'mr' } = ctx.params;
+ const link = `https://${language}.pornhub.com/pornstar/${username}/videos?o=${sort}`;
+ if (!isValidHost(language)) {
+ throw Error('Invalid language');
+ }
+
+ const { data: response } = await got(link, { headers });
+ const $ = cheerio.load(response);
+ const items = $('#mostRecentVideosSection .videoBox')
+ .toArray()
+ .map((e) => parseItems($(e)));
+
+ ctx.state.data = {
+ title: $('title').first().text(),
+ description: $('section.aboutMeSection').text().trim(),
+ link,
+ image: $('#coverPictureDefault').attr('src'),
+ logo: $('#getAvatar').attr('src'),
+ icon: $('#getAvatar').attr('src'),
+ language: $('html').attr('lang'),
+ item: items,
+ };
+};
diff --git a/lib/v2/pornhub/radar.js b/lib/v2/pornhub/radar.js
new file mode 100644
index 00000000000000..48913ac590cfa9
--- /dev/null
+++ b/lib/v2/pornhub/radar.js
@@ -0,0 +1,46 @@
+module.exports = {
+ 'pornhub.com': {
+ _name: 'PornHub',
+ '.': [
+ {
+ title: 'Category',
+ docs: 'https://docs.rsshub.app/routes/multimedia#pornhub',
+ source: ['/categories/:caty', '/video'],
+ target: (params, url) => {
+ if (params.caty) {
+ return `/pornhub/category/${params.caty}`;
+ }
+ return `/pornhub/category/${new URL(url).searchParams.get('c')}`;
+ },
+ },
+ {
+ title: 'Keyword Search',
+ docs: 'https://docs.rsshub.app/routes/multimedia#pornhub',
+ source: ['/video/search'],
+ target: (_, url) => `/pornhub/category/${new URL(url).searchParams.get('search')}`,
+ },
+ {
+ title: 'Users',
+ docs: 'https://docs.rsshub.app/routes/multimedia#pornhub',
+ source: ['/users/:username/*'],
+ target: '/pornhub/users/:username',
+ },
+ {
+ title: 'Verified amateur / Model',
+ docs: 'https://docs.rsshub.app/routes/multimedia#pornhub',
+ source: ['/model/:username/*'],
+ target: '/pornhub/model/:username',
+ },
+ {
+ title: 'Verified model / Pornstar',
+ docs: 'https://docs.rsshub.app/routes/multimedia#pornhub',
+ source: ['/pornstar/:username/*'],
+ target: '/pornhub/pornstar/:username',
+ },
+ {
+ title: 'Video List',
+ docs: 'https://docs.rsshub.app/routes/multimedia#pornhub',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/pornhub/router.js b/lib/v2/pornhub/router.js
new file mode 100644
index 00000000000000..4ebf5156dc4529
--- /dev/null
+++ b/lib/v2/pornhub/router.js
@@ -0,0 +1,8 @@
+module.exports = (router) => {
+ router.get('/category/:caty', require('./category'));
+ router.get('/search/:keyword', require('./search'));
+ router.get('/:language?/category_url/:url?', require('./category_url'));
+ router.get('/:language?/model/:username/:sort?', require('./model'));
+ router.get('/:language?/pornstar/:username/:sort?', require('./pornstar'));
+ router.get('/:language?/users/:username', require('./users'));
+};
diff --git a/lib/v2/pornhub/search.js b/lib/v2/pornhub/search.js
new file mode 100644
index 00000000000000..6aaa5d7d4b6416
--- /dev/null
+++ b/lib/v2/pornhub/search.js
@@ -0,0 +1,23 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { defaultDomain, renderDescription } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { keyword } = ctx.params;
+ const currentUrl = `${defaultDomain}/webmasters/search?search=${keyword}`;
+ const response = await got(currentUrl);
+
+ const list = response.data.videos.map((item) => ({
+ title: item.title,
+ link: item.url,
+ description: renderDescription({ thumbs: item.thumbs }),
+ pubDate: parseDate(item.publish_date),
+ category: [...new Set([...item.tags.map((t) => t.tag_name), ...item.categories.map((c) => c.category)])],
+ }));
+
+ ctx.state.data = {
+ title: `Pornhub - ${keyword}`,
+ link: currentUrl,
+ item: list,
+ };
+};
diff --git a/lib/v2/pornhub/templates/description.art b/lib/v2/pornhub/templates/description.art
new file mode 100644
index 00000000000000..33aea6d4e73fc0
--- /dev/null
+++ b/lib/v2/pornhub/templates/description.art
@@ -0,0 +1,11 @@
+{{ if previewVideo }}
+
+{{ /if }}
+
+{{ if thumbs }}
+ {{ each thumbs t }}
+
+ {{ /each }}
+{{ /if }}
diff --git a/lib/v2/pornhub/users.js b/lib/v2/pornhub/users.js
new file mode 100644
index 00000000000000..6fd8b98a3227d4
--- /dev/null
+++ b/lib/v2/pornhub/users.js
@@ -0,0 +1,30 @@
+const got = require('@/utils/got');
+const cheerio = require('cheerio');
+const { isValidHost } = require('@/utils/valid-host');
+const { headers, parseItems } = require('./utils');
+
+module.exports = async (ctx) => {
+ const { language = 'www', username } = ctx.params;
+ const link = `https://${language}.pornhub.com/users/${username}/videos`;
+ if (!isValidHost(language)) {
+ throw Error('Invalid language');
+ }
+
+ const { data: response } = await got(link, { headers });
+ const $ = cheerio.load(response);
+ const items = $('.videoUList .videoBox')
+ .toArray()
+ .map((e) => parseItems($(e)));
+
+ ctx.state.data = {
+ title: $('title').first().text(),
+ description: $('.aboutMeText').text().trim(),
+ link,
+ image: $('#coverPictureDefault').attr('src'),
+ logo: $('#getAvatar').attr('src'),
+ icon: $('#getAvatar').attr('src'),
+ language: $('html').attr('lang'),
+ allowEmpty: true,
+ item: items,
+ };
+};
diff --git a/lib/v2/pornhub/utils.js b/lib/v2/pornhub/utils.js
new file mode 100644
index 00000000000000..10d4af2b4663c3
--- /dev/null
+++ b/lib/v2/pornhub/utils.js
@@ -0,0 +1,30 @@
+const { art } = require('@/utils/render');
+const { join } = require('path');
+const { parseRelativeDate } = require('@/utils/parse-date');
+
+const defaultDomain = 'https://www.pornhub.com';
+
+const headers = {
+ accessAgeDisclaimerPH: 1,
+ hasVisited: 1,
+};
+
+const renderDescription = (data) => art(join(__dirname, 'templates/description.art'), data);
+
+const parseItems = (e) => ({
+ title: e.find('span.title a').text().trim(),
+ link: defaultDomain + e.find('span.title a').attr('href'),
+ description: renderDescription({
+ poster: e.find('img').data('mediumthumb'),
+ previewVideo: e.find('img').data('mediabook'),
+ }),
+ author: e.find('.usernameWrap a').text(),
+ pubDate: parseRelativeDate(e.find('.added').text()),
+});
+
+module.exports = {
+ defaultDomain,
+ headers,
+ renderDescription,
+ parseItems,
+};
diff --git a/website/docs/routes/multimedia.md b/website/docs/routes/multimedia.md
index 50fd691be6f974..543cb03b76df36 100644
--- a/website/docs/routes/multimedia.md
+++ b/website/docs/routes/multimedia.md
@@ -1119,41 +1119,45 @@ See [Directory](https://www.javlibrary.com/en/star_list.php) to view all stars.
### Category {#pornhub-category}
-
+
### Keyword Search {#pornhub-keyword-search}
-
+
### Users {#pornhub-users}
-
+
### Verified amateur / Model {#pornhub-verified-amateur-%2F-model}
-
+
### Verified model / Pornstar {#pornhub-verified-model-%2F-pornstar}
-
+
**`sort`**
-| mr | mv | tr | lg | cm |
-| ----------- | ----------- | --------- | ------- | ------ |
-| Most Recent | Most Viewed | Top Rated | Longest | Newest |
+| Most Recent | Most Viewed | Top Rated | Longest | Best |
+| ----------- | ----------- | --------- | ------- | ---- |
+| mr | mv | tr | lg | |
+
+
### Video List {#pornhub-video-list}
-
+
**`language`**
Refer to [Pornhub F.A.Qs](https://help.pornhub.com/hc/en-us/articles/360044327034-How-do-I-change-the-language-), English by default. For example:
-- `cn` (Chinese), for Pornhub in China ;
+- `cn` (Chinese), for Pornhub in China ;
-- `jp` (Japanese), for Pornhub in Japan etc.
+- `jp` (Japanese), for Pornhub in Japan etc.
+
+
## PRESTIGE(プレステージ 蚊香社) {#prestige(%E3%83%97%E3%83%AC%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B8-wen-xiang-she-)}