From 3d294baccb3a194bb8db0a858ebd09fadd101a06 Mon Sep 17 00:00:00 2001 From: ashi-koki Date: Fri, 10 Apr 2026 09:39:42 -0400 Subject: [PATCH 1/2] feat(route): add rule34video latest route --- lib/routes/rule34video/latest.ts | 134 ++++++++++++++++++++++++++++ lib/routes/rule34video/namespace.ts | 7 ++ 2 files changed, 141 insertions(+) create mode 100644 lib/routes/rule34video/latest.ts create mode 100644 lib/routes/rule34video/namespace.ts diff --git a/lib/routes/rule34video/latest.ts b/lib/routes/rule34video/latest.ts new file mode 100644 index 000000000000..673164b14f90 --- /dev/null +++ b/lib/routes/rule34video/latest.ts @@ -0,0 +1,134 @@ +import { load } from 'cheerio'; +import type { Context } from 'hono'; + +import type { Route } from '@/types'; +import got from '@/utils/got'; +import { parseRelativeDate } from '@/utils/parse-date'; + +export const route: Route = { + path: '/latest', + categories: ['multimedia'], + example: '/rule34video/latest', + description: 'Latest updates from Rule34 Video', + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + nsfw: true, + }, + radar: [ + { + source: ['rule34video.com/latest-updates/'], + target: '/rule34video/latest', + }, + ], + name: 'Latest Updates', + maintainers: ['Dgama'], + handler, +}; + +interface VideoItem { + title: string; + link: string; + preview?: string; + duration?: string; + added?: string; + rating?: string; + views?: string; + hasSound: boolean; + isHD: boolean; + videoId?: string; +} + +async function handler(_ctx: Context) { + const response = await got({ + method: 'get', + url: 'https://www.rule34video.com/latest-updates/', + headers: { + Referer: 'https://www.rule34video.com', + }, + }); + + const $ = load(response.data); + const items = $('a.th.js-open-popup') + .toArray() + .map((element) => { + const $el = $(element); + const title = $el.attr('title')?.trim() || $el.find('.thumb_title').text().trim(); + const link = $el.attr('href')?.trim() || ''; + const preview = $el.find('img.thumb.lazy-load').attr('data-original'); + const duration = $el.find('.time').text().trim() || undefined; + const added = $el.find('.added').text().replaceAll(/\s+/g, ' ').trim() || undefined; + const rating = $el.find('.rating').text().trim() || undefined; + const views = $el.find('.views').text().trim() || undefined; + const hasSound = $el.find('.sound').length > 0; + const isHD = $el.find('.quality').length > 0; + const videoId = link.match(/\/video\/(\d+)\//)?.[1]; + + return { + title, + link, + preview, + duration, + added, + rating, + views, + hasSound, + isHD, + videoId, + } as VideoItem; + }) + .filter((item) => item.title && item.link); + + return { + allowEmpty: true, + title: 'Rule34 Video Latest Updates', + link: 'https://www.rule34video.com/latest-updates/', + description: 'Latest updates from Rule34 Video', + item: items.map((item) => buildDataItem(item)), + }; +} + +function buildDataItem(item: VideoItem) { + let description = ''; + + if (item.duration) { + description += `

Duration: ${item.duration}

`; + } + if (item.views) { + description += `

Views: ${item.views}

`; + } + if (item.rating) { + description += `

Rating: ${item.rating}

`; + } + + const qualities: string[] = []; + if (item.isHD) { + qualities.push('HD'); + } + if (item.hasSound) { + qualities.push('Has Sound'); + } + if (qualities.length > 0) { + description += `

Quality: ${qualities.join(', ')}

`; + } + + if (item.preview) { + description += `${item.title}`; + } + + const pubDate = item.added ? parseRelativeDate(item.added) : undefined; + + return { + title: item.title, + link: item.link, + description, + image: item.preview, + ...(pubDate && { pubDate: pubDate.toISOString() }), + guid: item.videoId ? `rule34video:${item.videoId}` : item.link, + category: item.isHD ? ['HD'] : [], + }; +} diff --git a/lib/routes/rule34video/namespace.ts b/lib/routes/rule34video/namespace.ts new file mode 100644 index 000000000000..ec16c6d0af4b --- /dev/null +++ b/lib/routes/rule34video/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Rule34Video', + url: 'rule34video.com', + lang: 'en', +}; \ No newline at end of file From d8cf6c1620320de8f2a565614974dc771b6e609c Mon Sep 17 00:00:00 2001 From: ashi-koki Date: Sat, 18 Apr 2026 10:34:17 -0400 Subject: [PATCH 2/2] fix(route): reduce redundant returns, use suggested full route path --- lib/routes/rule34video/latest.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/routes/rule34video/latest.ts b/lib/routes/rule34video/latest.ts index 673164b14f90..4440d2cf6072 100644 --- a/lib/routes/rule34video/latest.ts +++ b/lib/routes/rule34video/latest.ts @@ -1,6 +1,6 @@ import { load } from 'cheerio'; -import type { Context } from 'hono'; +import { config } from '@/config'; import type { Route } from '@/types'; import got from '@/utils/got'; import { parseRelativeDate } from '@/utils/parse-date'; @@ -22,11 +22,11 @@ export const route: Route = { radar: [ { source: ['rule34video.com/latest-updates/'], - target: '/rule34video/latest', + target: '/latest', }, ], name: 'Latest Updates', - maintainers: ['Dgama'], + maintainers: ['ashi-koki'], handler, }; @@ -43,11 +43,12 @@ interface VideoItem { videoId?: string; } -async function handler(_ctx: Context) { +async function handler() { const response = await got({ method: 'get', url: 'https://www.rule34video.com/latest-updates/', headers: { + 'User-Agent': config.trueUA, Referer: 'https://www.rule34video.com', }, }); @@ -60,10 +61,10 @@ async function handler(_ctx: Context) { const title = $el.attr('title')?.trim() || $el.find('.thumb_title').text().trim(); const link = $el.attr('href')?.trim() || ''; const preview = $el.find('img.thumb.lazy-load').attr('data-original'); - const duration = $el.find('.time').text().trim() || undefined; - const added = $el.find('.added').text().replaceAll(/\s+/g, ' ').trim() || undefined; - const rating = $el.find('.rating').text().trim() || undefined; - const views = $el.find('.views').text().trim() || undefined; + const duration = $el.find('.time').text().trim(); + const added = $el.find('.added').text().replaceAll(/\s+/g, ' ').trim(); + const rating = $el.find('.rating').text().trim(); + const views = $el.find('.views').text().trim(); const hasSound = $el.find('.sound').length > 0; const isHD = $el.find('.quality').length > 0; const videoId = link.match(/\/video\/(\d+)\//)?.[1];