Skip to content

Commit c0785bd

Browse files
eve2ptpgithub-actions[bot]Evilmass
authored
feat: add xhamster (#21640)
* feat xhamster * style: auto format * add maintainers * remove useless header * replace html() to text() * Use U+002d instead of U+2013 * Use regex to match the string instead of replacing it * no author * remove link & author from description * remove caching * Apply suggestion --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Evilmass <21218296+Evilmass@users.noreply.github.com>
1 parent c16c258 commit c0785bd

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

lib/routes/xhamster/index.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { load } from 'cheerio';
2+
import type { Route } from '@/types';
3+
import got from '@/utils/got';
4+
import { parseDate } from '@/utils/parse-date';
5+
6+
export const route: Route = {
7+
path: '/:creators',
8+
categories: ['multimedia'],
9+
example: '/xhamster/faustina-pierre',
10+
parameters: {
11+
creators: 'Creator slug from the URL (e.g. `faustina-pierre`)',
12+
},
13+
features: {
14+
requireConfig: false,
15+
requirePuppeteer: false,
16+
antiCrawler: false,
17+
supportBT: false,
18+
supportPodcast: false,
19+
supportScihub: false,
20+
nsfw: true,
21+
},
22+
radar: [
23+
{
24+
source: ['xhamster.com/creators/:creators', 'xhamster.com/creators/:creators/newest'],
25+
target: '/:creators',
26+
},
27+
],
28+
name: 'Newest Videos by Creator',
29+
maintainers: ['eve2ptp'],
30+
handler,
31+
url: 'xhamster.com/faustina-pierre/newest',
32+
};
33+
34+
interface VideoThumb {
35+
id: number;
36+
title: string;
37+
pageURL: string;
38+
thumbURL: string;
39+
imageURL?: string;
40+
trailerURL?: string;
41+
trailerFallbackUrl?: string;
42+
created?: number;
43+
duration?: number;
44+
views?: number;
45+
isUHD?: boolean;
46+
}
47+
48+
interface Initials {
49+
infoComponent?: {
50+
pornstarTop?: {
51+
name?: string;
52+
};
53+
};
54+
trendingVideoSectionComponent?: {
55+
videoListProps?: {
56+
videoThumbProps?: VideoThumb[];
57+
};
58+
};
59+
}
60+
61+
function extractInitials(scriptContent: string): Initials {
62+
const match = scriptContent.match(/window\.initials\s*=\s*([\s\S]*?);?$/);
63+
if (!match) {
64+
throw new Error('initials not found');
65+
}
66+
return JSON.parse(match[1]);
67+
}
68+
69+
function formatDuration(seconds: number): string {
70+
const h = Math.floor(seconds / 3600);
71+
const m = Math.floor((seconds % 3600) / 60);
72+
const s = seconds % 60;
73+
return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`;
74+
}
75+
76+
function renderDescription(video: VideoThumb): string {
77+
const thumb = video.imageURL ?? video.thumbURL;
78+
const duration = video.duration ? formatDuration(video.duration) : '';
79+
const views = video.views === undefined ? '' : video.views.toLocaleString();
80+
const quality = video.isUHD ? '<span>4K</span>' : '';
81+
82+
let meta = "";
83+
84+
if (quality) {
85+
meta += quality;
86+
}
87+
if (duration) {
88+
meta += `${meta ? " · " : ""}<strong>Duration:</strong> ${duration}`;
89+
}
90+
if (views) {
91+
meta += `${meta ? " · " : ""}<strong>Views:</strong> ${views}`;
92+
}
93+
94+
return `
95+
<img src="${thumb}" alt="${video.title}" style="max-width:100%" />
96+
<p>${meta}</p>
97+
`.trim();
98+
}
99+
100+
async function handler(ctx) {
101+
const { creators } = ctx.req.param();
102+
const pageUrl = `https://xhamster.com/creators/${encodeURIComponent(creators)}/newest`;
103+
104+
const response = await got(pageUrl);
105+
106+
const $ = load(response.data);
107+
const initialsRaw = $('#initials-script').text();
108+
if (!initialsRaw) {
109+
throw new Error('Could not locate initials script on page');
110+
}
111+
112+
let initials: Initials;
113+
try {
114+
initials = extractInitials(initialsRaw);
115+
} catch {
116+
throw new Error('Failed to parse page data');
117+
}
118+
119+
const creatorName = initials.infoComponent?.pornstarTop?.name ?? creators;
120+
const videos = initials.trendingVideoSectionComponent?.videoListProps?.videoThumbProps ?? [];
121+
122+
const items = videos.map((video) => ({
123+
title: `${video.title}${video.isUHD ? ' [4K]' : ''}`,
124+
link: video.pageURL,
125+
pubDate: video.created ? parseDate(video.created * 1000) : undefined,
126+
author: creatorName,
127+
description: renderDescription(video),
128+
media: {
129+
content: {
130+
url: video.trailerURL ?? video.pageURL,
131+
type: 'video/mp4',
132+
...(video.duration && { duration: video.duration }),
133+
},
134+
thumbnail: {
135+
url: video.imageURL ?? video.thumbURL,
136+
},
137+
},
138+
}));
139+
140+
return {
141+
title: `${creatorName} - newest videos on xHamster`,
142+
link: pageUrl,
143+
description: `Latest videos from ${creatorName} on xHamster`,
144+
item: items,
145+
};
146+
}

lib/routes/xhamster/namespace.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Namespace } from '@/types';
2+
3+
export const namespace: Namespace = {
4+
name: 'xHamster',
5+
url: 'xhamster.com',
6+
lang: 'en',
7+
};

0 commit comments

Comments
 (0)