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; +};