diff --git a/lib/routes/javtrailers/casts.ts b/lib/routes/javtrailers/casts.ts
new file mode 100644
index 00000000000000..f8c3bceca40040
--- /dev/null
+++ b/lib/routes/javtrailers/casts.ts
@@ -0,0 +1,41 @@
+import { Route } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { baseUrl, getItem, headers, parseList } from './utils';
+
+export const route: Route = {
+ path: '/casts/:cast',
+ categories: ['multimedia'],
+ example: '/javtrailers/casts/hibiki-otsuki',
+ parameters: { cast: 'Cast name, can be found in the URL of the cast page' },
+ radar: [
+ {
+ source: ['javtrailers.com/casts/:category'],
+ },
+ ],
+ name: 'Casts',
+ maintainers: ['TonyRL'],
+ url: 'javtrailers.com/casts',
+ handler,
+};
+
+async function handler(ctx) {
+ const { cast } = ctx.req.param();
+
+ const response = await ofetch(`${baseUrl}/api/casts/${cast}?page=0`, {
+ headers,
+ });
+
+ const list = parseList(response.videos);
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item))));
+
+ return {
+ title: `Watch ${response.cast.name} Jav Online | Japanese Adult Video - JavTrailers.com`,
+ description: response.cast.castWiki?.description.replaceAll('\n', ' ') ?? `Watch ${response.cast.name} Jav video’s free, we have the largest Jav collections with high definition`,
+ image: response.cast.avatar,
+ link: `${baseUrl}/casts/${cast}`,
+ item: items,
+ };
+}
diff --git a/lib/routes/javtrailers/categories.ts b/lib/routes/javtrailers/categories.ts
new file mode 100644
index 00000000000000..402e366b3a2292
--- /dev/null
+++ b/lib/routes/javtrailers/categories.ts
@@ -0,0 +1,40 @@
+import { Route } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { baseUrl, getItem, headers, parseList } from './utils';
+
+export const route: Route = {
+ path: '/categories/:category',
+ categories: ['multimedia'],
+ example: '/javtrailers/categories/50001755',
+ parameters: { category: 'Category name, can be found in the URL of the category page' },
+ radar: [
+ {
+ source: ['javtrailers.com/categories/:category'],
+ },
+ ],
+ name: 'Categories',
+ maintainers: ['TonyRL'],
+ url: 'javtrailers.com/categories',
+ handler,
+};
+
+async function handler(ctx) {
+ const { category } = ctx.req.param();
+
+ const response = await ofetch(`${baseUrl}/api/categories/${category}?page=0`, {
+ headers,
+ });
+
+ const list = parseList(response.videos);
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item))));
+
+ return {
+ title: `Watch ${response.category.name} Jav Online | Japanese Adult Video - JavTrailers.com`,
+ description: `Watch ${response.category.name} Jav video’s free, we have the largest Jav collections with high definition`,
+ link: `${baseUrl}/categories/${category}`,
+ item: items,
+ };
+}
diff --git a/lib/routes/javtrailers/namespace.ts b/lib/routes/javtrailers/namespace.ts
new file mode 100644
index 00000000000000..48e5c45912fb15
--- /dev/null
+++ b/lib/routes/javtrailers/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'JavTrailers',
+ url: 'javtrailers.com',
+ lang: 'ja',
+};
diff --git a/lib/routes/javtrailers/studios.ts b/lib/routes/javtrailers/studios.ts
new file mode 100644
index 00000000000000..72ebfca6c6d881
--- /dev/null
+++ b/lib/routes/javtrailers/studios.ts
@@ -0,0 +1,39 @@
+import { Route } from '@/types';
+
+import ofetch from '@/utils/ofetch';
+import cache from '@/utils/cache';
+import { baseUrl, getItem, headers, parseList } from './utils';
+
+export const route: Route = {
+ path: '/studios/:studio',
+ categories: ['multimedia'],
+ example: '/javtrailers/studios/s1-no-1-style',
+ parameters: { studio: 'Studio name, can be found in the URL of the studio page' },
+ radar: [
+ {
+ source: ['javtrailers.com/studios/:category'],
+ },
+ ],
+ name: 'Studios',
+ maintainers: ['TonyRL'],
+ handler,
+};
+
+async function handler(ctx) {
+ const { studio } = ctx.req.param();
+
+ const response = await ofetch(`${baseUrl}/api/studios/${studio}?page=0`, {
+ headers,
+ });
+
+ const list = parseList(response.videos);
+
+ const items = await Promise.all(list.map((item) => cache.tryGet(item.link, () => getItem(item))));
+
+ return {
+ title: `${response.studio.hotDvdIds?.join(' ') ?? response.studio.name} Jav Online | Japanese Adult Video - JavTrailers.com`,
+ description: 'Watch Jav made by Prestige free, with high definition, we have over 4,000 studios available for free streaming.',
+ link: `${baseUrl}/studios/${studio}`,
+ item: items,
+ };
+}
diff --git a/lib/routes/javtrailers/templates/description.art b/lib/routes/javtrailers/templates/description.art
new file mode 100644
index 00000000000000..36b47a10240a94
--- /dev/null
+++ b/lib/routes/javtrailers/templates/description.art
@@ -0,0 +1,31 @@
+{{ if videoInfo.image }}
+
+{{ /if }}
+
+{{ if videoInfo.dvdId }}DVD ID: {{ videoInfo.dvdId }}
{{ /if }}
+{{ if videoInfo.contentId }}Content ID: {{ videoInfo.contentId }}
{{ /if }}
+{{ if videoInfo.releaseDate }}Release Date: {{ videoInfo.releaseDate }}
{{ /if }}
+{{ if videoInfo.duration }}Duration: {{ videoInfo.duration }} mins
{{ /if }}
+{{ if videoInfo.director }}Director: {{ videoInfo.director }} {{ videoInfo.jpDirector }}
{{ /if }}
+{{ if videoInfo.studio }}Studio: {{ videoInfo.studio.name }}
{{ /if }}
+{{ if videoInfo.categories }}
+ Categories:
+ {{ each videoInfo.categories c }}
+ {{ c.name }},
+ {{ /each }}
+
+{{ /if }}
+{{ if videoInfo.casts }}
+ Cast(s):
+ {{ each videoInfo.casts c }}
+ {{ c.name }} {{ c.jpName }}
+ {{ /each }}
+
+{{ /if }}
+
+
+{{ if videoInfo.gallery }}
+ {{ each videoInfo.gallery g }}
+
+ {{ /each }}
+{{ /if }}
diff --git a/lib/routes/javtrailers/types.ts b/lib/routes/javtrailers/types.ts
new file mode 100644
index 00000000000000..2ac2e1eb4c5403
--- /dev/null
+++ b/lib/routes/javtrailers/types.ts
@@ -0,0 +1,59 @@
+export interface Video {
+ _id: string;
+ categories: Category[];
+ casts: Cast[];
+ director: string;
+ gallery: string[];
+ title: string;
+ javLink: JavLink;
+ contentId: string;
+ dvdId: string;
+ studio: Studio;
+ releaseDate: string;
+ duration: number;
+ image: string;
+ jpDirector: string;
+ jpTitle: string;
+ /**
+ * HLS stream URL
+ */
+ trailer: string;
+ zhTitle: string;
+ __v: number;
+}
+
+interface Category {
+ _id: string;
+ slug: string;
+ name: string;
+ jpName: string;
+ zhName: string;
+}
+
+interface Cast {
+ _id: string;
+ slug: string;
+ ruby: string;
+ link: string;
+ name: string;
+ jpName: string;
+ avatar: string;
+ __v: number;
+}
+
+interface JavLink {
+ _id: string;
+ link: string;
+ processed: boolean;
+ isProfessional: boolean;
+ upcoming: boolean;
+ __v: number;
+}
+
+interface Studio {
+ _id: string;
+ slug: string;
+ name: string;
+ link: string;
+ jpName: string;
+}
diff --git a/lib/routes/javtrailers/utils.ts b/lib/routes/javtrailers/utils.ts
new file mode 100644
index 00000000000000..5b8037bef24240
--- /dev/null
+++ b/lib/routes/javtrailers/utils.ts
@@ -0,0 +1,48 @@
+import { Video } from './types';
+
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+import { art } from '@/utils/render';
+import path from 'node:path';
+import { getCurrentPath } from '@/utils/helpers';
+const __dirname = getCurrentPath(import.meta.url);
+
+export const baseUrl = 'https://javtrailers.com';
+export const headers = {
+ Authorization: 'AELAbPQCh_fifd93wMvf_kxMD_fqkUAVf@BVgb2!md@TNW8bUEopFExyGCoKRcZX',
+};
+
+export const hdGallery = (gallery) =>
+ gallery.map((item) => {
+ if (item.startsWith('https://pics.dmm.co.jp/')) {
+ return item.replace(/-(\d+)\.jpg$/, 'jp-$1.jpg');
+ } else if (item.startsWith('https://image.mgstage.com/')) {
+ return item.replace(/cap_t1_/, 'cap_e_');
+ }
+ return item;
+ });
+
+export const parseList = (videos) =>
+ videos.map((item) => ({
+ title: `${item.dvdId} ${item.title}`,
+ link: `${baseUrl}/video/${item.contentId}`,
+ pubDate: parseDate(item.releaseDate),
+ contentId: item.contentId,
+ }));
+
+export const getItem = async (item) => {
+ const response = await ofetch(`${baseUrl}/api/video/${item.contentId}`, {
+ headers,
+ });
+
+ const videoInfo: Video = response.video;
+ videoInfo.gallery = hdGallery(videoInfo.gallery);
+
+ item.description = art(path.join(__dirname, 'templates/description.art'), {
+ videoInfo,
+ });
+ item.author = videoInfo.casts.map((cast) => `${cast.name} ${cast.jpName}`).join(', ');
+ item.category = videoInfo.categories.map((category) => `${category.name}/${category.jpName}/${category.zhName}`);
+
+ return item;
+};