From 72d4ee7ab4fbacae52ed6ebf7708b70b8739bf02 Mon Sep 17 00:00:00 2001 From: Exat1979 Date: Sat, 11 Apr 2026 16:14:37 +0200 Subject: [PATCH] feat: add projectjav actress (#21698) --- lib/routes/projectjav/actress.ts | 36 +++++++++++ lib/routes/projectjav/namespace.ts | 8 +++ lib/routes/projectjav/utils.ts | 97 ++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 lib/routes/projectjav/actress.ts create mode 100644 lib/routes/projectjav/namespace.ts create mode 100644 lib/routes/projectjav/utils.ts diff --git a/lib/routes/projectjav/actress.ts b/lib/routes/projectjav/actress.ts new file mode 100644 index 000000000..4c9b9a94e --- /dev/null +++ b/lib/routes/projectjav/actress.ts @@ -0,0 +1,36 @@ +import type { Route } from '@/types'; + +import { processItems, rootUrl } from './utils'; + +export const route: Route = { + path: '/actress/:id', + categories: ['multimedia'], + example: '/projectjav/actress/rima-arai-22198', + parameters: { id: 'Actress ID or slug, can be found in the actress page URL' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + nsfw: true, + }, + radar: [ + { + source: ['projectjav.com/actress/:id'], + target: '/actress/:id', + }, + ], + name: 'Actress', + maintainers: ['Exat1979'], + handler, + url: 'projectjav.com/', + description: 'Fetches the latest movies from a specific actress page on ProjectJAV.', +}; + +async function handler(ctx) { + const id = ctx.req.param('id').replace(/\/$/, ''); + const currentUrl = `${rootUrl}/actress/${id}`; + return await processItems(currentUrl); +} diff --git a/lib/routes/projectjav/namespace.ts b/lib/routes/projectjav/namespace.ts new file mode 100644 index 000000000..9581f6264 --- /dev/null +++ b/lib/routes/projectjav/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'ProjectJAV', + url: 'projectjav.com', + description: 'ProjectJAV provides adult video content information and streaming.', + lang: 'en', +}; diff --git a/lib/routes/projectjav/utils.ts b/lib/routes/projectjav/utils.ts new file mode 100644 index 000000000..d863ebc7b --- /dev/null +++ b/lib/routes/projectjav/utils.ts @@ -0,0 +1,97 @@ +import { load } from 'cheerio'; + +import type { DataItem } from '@/types'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +const rootUrl = 'https://projectjav.com'; +const processItems = async (currentUrl: string) => { + const response = await got({ + method: 'get', + url: currentUrl, + }); + + const $ = load(response.data); + + let items: DataItem[] = $('div.video-item') + .toArray() + .map((element) => { + const item = $(element); + const link = item.find('a').attr('href'); + return { + title: item.find('div.name span').text() || '', + link: link?.startsWith('http') ? link : `${rootUrl}${link}`, + }; + }) + .filter((item) => item.link && /\/movie\/.*/.test(item.link)); + + items = await Promise.all( + items.map((item) => + cache.tryGet(item.link!, async () => { + const detailResponse = await got({ + method: 'get', + url: item.link, + }); + + const content = load(detailResponse.data); + + // Remove ads + content('div.top-banner-ads, div.bottom-content-ads').remove(); + + // Get main content + const mainContent = content('main'); + + // Extract title + const h1Title = mainContent.find('h1').text().trim(); + if (h1Title) { + item.title = h1Title; + } + + // Extract categories + const categories = mainContent + .find('div.badge a') + .toArray() + .map((v) => content(v).text().trim()) + .filter(Boolean); + if (categories.length > 0) { + item.category = categories; + } + + // Extract author/actress (support multiple actresses) + const actresses = mainContent + .find('div.actress-item a') + .toArray() + .map((v) => content(v).text().trim()) + .filter(Boolean); + if (actresses.length > 0) { + item.author = actresses.join(', '); + } + + // Extract date + const dateEl = mainContent + .find('div.second-main~div.row>div.col-3') + .toArray() + .find((el) => content(el).text().includes('Date added')); + const dateText = dateEl ? content(dateEl).next().text().trim() : null; + if (dateText) { + // ProjectJAV uses DD/MM/YYYY format + item.pubDate = parseDate(dateText, 'DD/MM/YYYY'); + } + + // Get description + item.description = mainContent.html() || ''; + + return item; + }) + ) + ); + + return { + title: $('title').text() || 'ProjectJAV', + link: currentUrl, + item: items, + }; +}; + +export { processItems, rootUrl };