From 68b9acb9698615236039efdfa2ed95b9fb4f6675 Mon Sep 17 00:00:00 2001 From: Exat1979 Date: Fri, 7 Nov 2025 18:37:56 +0100 Subject: [PATCH 1/4] feat: implement projectjav actress route correctly handle trailing slash --- lib/routes/projectjav/actress.ts | 36 ++++++++++ lib/routes/projectjav/namespace.ts | 8 +++ lib/routes/projectjav/utils.ts | 101 +++++++++++++++++++++++++++++ 3 files changed, 145 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 000000000000..b542cf7af5c6 --- /dev/null +++ b/lib/routes/projectjav/actress.ts @@ -0,0 +1,36 @@ +import { Route } from '@/types'; +import cache from '@/utils/cache'; +import { rootUrl, processItems } from './utils'; + +export const route: Route = { + path: ['/actress/:id', '/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'); + const currentUrl = `${rootUrl}/actress/${id}`; + return await processItems(currentUrl, cache.tryGet); +} diff --git a/lib/routes/projectjav/namespace.ts b/lib/routes/projectjav/namespace.ts new file mode 100644 index 000000000000..9581f6264ed0 --- /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 000000000000..4b0a9fb26271 --- /dev/null +++ b/lib/routes/projectjav/utils.ts @@ -0,0 +1,101 @@ +import got from '@/utils/got'; +import { load } from 'cheerio'; +import { parseDate } from '@/utils/parse-date'; +import type { DataItem } from '@/types'; + +const rootUrl = 'https://projectjav.com'; +const processItems = async (currentUrl: string, tryGet) => { + 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) => + 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 + let dateText: string | null = null; + mainContent + .find('div.second-main~div.row>div.col-3') + .toArray() + .some((el) => { + if (content(el).text().includes('Date added')) { + dateText = content(el).next().text().trim(); + return true; + } + return false; + }); + 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 }; From ff85abe46acfb2ace03ed69663e9e7d26216dac9 Mon Sep 17 00:00:00 2001 From: Alistair1231 Date: Wed, 8 Apr 2026 22:53:05 +0200 Subject: [PATCH 2/4] fix linter errors --- lib/routes/projectjav/actress.ts | 5 +++-- lib/routes/projectjav/utils.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/routes/projectjav/actress.ts b/lib/routes/projectjav/actress.ts index b542cf7af5c6..ae8226fcc05d 100644 --- a/lib/routes/projectjav/actress.ts +++ b/lib/routes/projectjav/actress.ts @@ -1,6 +1,7 @@ -import { Route } from '@/types'; +import type { Route } from '@/types'; import cache from '@/utils/cache'; -import { rootUrl, processItems } from './utils'; + +import { processItems, rootUrl } from './utils'; export const route: Route = { path: ['/actress/:id', '/actress/:id/'], diff --git a/lib/routes/projectjav/utils.ts b/lib/routes/projectjav/utils.ts index 4b0a9fb26271..7ced4f6d304a 100644 --- a/lib/routes/projectjav/utils.ts +++ b/lib/routes/projectjav/utils.ts @@ -1,7 +1,8 @@ -import got from '@/utils/got'; import { load } from 'cheerio'; -import { parseDate } from '@/utils/parse-date'; + import type { DataItem } from '@/types'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; const rootUrl = 'https://projectjav.com'; const processItems = async (currentUrl: string, tryGet) => { From da21d249043c64d5b9855e345dffa94c3f3fa676 Mon Sep 17 00:00:00 2001 From: Alistair1231 Date: Wed, 8 Apr 2026 22:57:56 +0200 Subject: [PATCH 3/4] import cache directly instead of passing it --- lib/routes/projectjav/actress.ts | 5 ++--- lib/routes/projectjav/utils.ts | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/routes/projectjav/actress.ts b/lib/routes/projectjav/actress.ts index ae8226fcc05d..6ad8d181116f 100644 --- a/lib/routes/projectjav/actress.ts +++ b/lib/routes/projectjav/actress.ts @@ -1,10 +1,9 @@ import type { Route } from '@/types'; -import cache from '@/utils/cache'; import { processItems, rootUrl } from './utils'; export const route: Route = { - path: ['/actress/:id', '/actress/:id/'], + 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' }, @@ -33,5 +32,5 @@ export const route: Route = { async function handler(ctx) { const id = ctx.req.param('id'); const currentUrl = `${rootUrl}/actress/${id}`; - return await processItems(currentUrl, cache.tryGet); + return await processItems(currentUrl); } diff --git a/lib/routes/projectjav/utils.ts b/lib/routes/projectjav/utils.ts index 7ced4f6d304a..1a7024de3f47 100644 --- a/lib/routes/projectjav/utils.ts +++ b/lib/routes/projectjav/utils.ts @@ -1,11 +1,12 @@ 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, tryGet) => { +const processItems = async (currentUrl: string) => { const response = await got({ method: 'get', url: currentUrl, @@ -27,7 +28,7 @@ const processItems = async (currentUrl: string, tryGet) => { items = await Promise.all( items.map((item) => - tryGet(item.link!, async () => { + cache.tryGet(item.link!, async () => { const detailResponse = await got({ method: 'get', url: item.link, From be2560ad5946f5db8b6adbe6073d8211b8a85e26 Mon Sep 17 00:00:00 2001 From: Alistair1231 Date: Thu, 9 Apr 2026 08:53:20 +0200 Subject: [PATCH 4/4] projectjav: add support for trailing slash --- lib/routes/projectjav/actress.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/routes/projectjav/actress.ts b/lib/routes/projectjav/actress.ts index 6ad8d181116f..ee72280a7116 100644 --- a/lib/routes/projectjav/actress.ts +++ b/lib/routes/projectjav/actress.ts @@ -3,7 +3,7 @@ import type { Route } from '@/types'; import { processItems, rootUrl } from './utils'; export const route: Route = { - path: '/actress/:id', + 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' }, @@ -30,7 +30,7 @@ export const route: Route = { }; async function handler(ctx) { - const id = ctx.req.param('id'); + const id = ctx.req.param('id').replace(/\/$/, ''); const currentUrl = `${rootUrl}/actress/${id}`; return await processItems(currentUrl); }