feat: add Eporner and Txxx along with minor fixes (#16)

Co-authored-by: Pi Patel <pi.patel@gmail.com>
This commit is contained in:
Lame-Fortuna
2026-03-12 16:21:04 +05:30
committed by GitHub
parent ac1a6d82fb
commit 09b49f1649
23 changed files with 984 additions and 130 deletions

View File

@@ -0,0 +1,66 @@
import { scrapeContent } from "../../scraper/eporner/epornerGetController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function getEporner(req: Request, res: Response) {
try {
const id = req.query.id as string;
if (!id) throw Error("Parameter id is required");
/**
* @api {get} /eporner/get?id=:id Get eporner
* @apiName Get eporner
* @apiGroup eporner
* @apiDescription Get a eporner video based on id
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/eporner/get?id=ibvqvezXzcs
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/eporner/get?id=ibvqvezXzcs")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/eporner/get?id=ibvqvezXzcs") as resp:
* print(await resp.json())
*/
// https://www.eporner.com/video-ibvqvezXzcs/ivy-seduces-her-dad-s-friend/
// https://www.eporner.com/hd-porn/hgovoiPexQe/Risa-Tsukino/
let path: string;
if (id.startsWith("video-")) {
path = id;
} else {
path = `hd-porn/${id}`;
}
const url = `${c.EPORNER}/${path}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -0,0 +1,28 @@
import { scrapeContent } from "../../scraper/eporner/epornerGetRelatedController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
export async function relatedEporner(req: Request, res: Response) {
try {
const id = req.query.id as string;
if (!id) throw Error("Parameter id is required");
const url = `${c.EPORNER}/video-${id}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -0,0 +1,75 @@
import { scrapeContent as searchScrape } from "../../scraper/eporner/epornerSearchController";
import { scrapeContent as videoScrape } from "../../scraper/eporner/epornerGetController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError } from "../../utils/modifier";
import { Request, Response } from "express";
import LustPress from "../../LustPress";
const lust = new LustPress();
export async function randomEporner(req: Request, res: Response) {
/**
* @api {get} /eporner/random Get random eporner
* @apiName Get random eporner
* @apiGroup eporner
* @apiDescription Get a random eporner video
*
* @apiParam {String} id Video ID
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/eporner/random
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/eporner/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/eporner/random") as resp:
* print(await resp.json())
*/
// cat/all/SORT-top-weekly/
try {
const weeklyUrl = `${c.EPORNER}/cat/all/SORT-top-weekly/`;
const list = await searchScrape(weeklyUrl);
if (!list.data.length) {
throw new Error("No weekly top videos found");
}
const random = list.data[Math.floor(Math.random() * list.data.length)];
let path: string;
if (random.id.startsWith("video-")) {
path = random.id;
} else {
path = `hd-porn/${random.id}`;
}
const url = `${c.EPORNER}/${path}`;
const data = await videoScrape(url);
logger.info({
path: req.path,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json(data);
} catch (err) {
res.status(400).json(maybeError(false, (err as Error).message));
}
}

View File

@@ -0,0 +1,72 @@
import { scrapeContent } from "../../scraper/eporner/epornerSearchController";
import c from "../../utils/options";
import { logger } from "../../utils/logger";
import { maybeError, spacer } from "../../utils/modifier";
import { Request, Response } from "express";
export async function searchEporner(req: Request, res: Response) {
try {
/**
* @api {get} /eporner/search Search eporner videos
* @apiName Search eporner
* @apiGroup eporner
* @apiDescription Search eporner videos
* @apiParam {String} key Keyword to search
* @apiParam {Number} [page=1] Page number
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/eporner/search?key=milf
* curl -i https://lust.scathach.id/eporner/search?key=milf&page=2
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
* axios.get("https://lust.scathach.id/eporner/search?key=milf")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/eporner/search?key=milf") as resp:
* print(await resp.json())
*/
// https://www.eporner.com/tag/milf/
const key = req.query.key as string;
const page = Number(req.query.page || 1);
if (!key) throw Error("Parameter key is required");
if (isNaN(page)) throw Error("Parameter page must be a number");
const slug = key
.toLowerCase()
.trim()
.replace(/\s+/g, "-");
const url =
page === 1
? `${c.EPORNER}/tag/${slug}/`
: `${c.EPORNER}/tag/${slug}/${page}/`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
});
return res.json(data);
} catch (err) {
const e = err as Error;
res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -0,0 +1,82 @@
import { Request, Response } from "express";
import LustPress from "../../LustPress";
import { maybeError } from "../../utils/modifier";
import { logger } from "../../utils/logger";
import { IVideoData } from "../../interfaces";
const lust = new LustPress();
// Generate sharded API URL exactly like TXXX expects
function getApiUrl(videoId: string): string {
const id = Number(videoId);
if (Number.isNaN(id)) throw new Error("Invalid video id");
const million = Math.floor(id / 1_000_000) * 1_000_000;
const thousand = Math.floor(id / 1_000) * 1_000;
return `https://txxx.com/api/json/video/86400/${million}/${thousand}/${id}.json`;
}
export async function getTxxx(req: Request, res: Response) {
try {
const id = String(req.query.id || "").trim();
if (!id) throw new Error("Parameter id is required");
const apiUrl = getApiUrl(id);
const buffer = await lust.fetchBody(apiUrl);
const parsed = JSON.parse(buffer.toString("utf-8"));
if (!parsed?.video) {
throw new Error("Invalid API response");
}
const video = parsed.video;
const categories = Object.values(video.categories || {}).map(
(c: any) => c.title,
);
const tags = Object.values(video.tags || {}).map((t: any) => t.title);
const models = Object.values(video.models || {}).map((m: any) => m.title);
const videoDir = video.dir || "";
const videoId = video.video_id;
const embed = `https://txxx.com/embed/${videoId}/`;
const response: IVideoData = {
success: true,
data: {
title: video.title || "None",
id: `${videoId}`,
image: video.thumbsrc || video.thumb || "None",
duration: video.duration || "None",
views: video.statistics?.viewed || "0",
rating: video.statistics?.rating || "0.00",
uploaded: video.post_date || "None",
upvoted: String(video.statistics?.likes || "0"),
downvoted: String(video.statistics?.dislikes || "0"),
channel: video.channel?.title || "",
models: models.length > 0 ? models : video.models_suggested || [],
tags: [...categories, ...tags],
},
source: `https://txxx.com/videos/${videoId}/${videoDir}/`,
assets: [embed, video.thumbsrc].filter(Boolean),
};
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json(response);
} catch (err) {
const e = err as Error;
return res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -0,0 +1,69 @@
import { Request, Response } from "express";
import LustPress from "../../LustPress";
import { maybeError } from "../../utils/modifier";
import { logger } from "../../utils/logger";
import { ISearchVideoData } from "../../interfaces";
const lust = new LustPress();
function getRelatedApiUrl(videoId: string, page = 1, count = 50): string {
const id = Number(videoId);
if (Number.isNaN(id)) throw new Error("Invalid video id");
const million = Math.floor(id / 1_000_000) * 1_000_000;
const thousand = Math.floor(id / 1_000) * 1_000;
return (
`https://txxx.com/api/json/videos_related2/` +
`432000/${count}/${million}/${thousand}/${id}.all.${page}.json`
);
}
export async function relatedTxxx(req: Request, res: Response) {
try {
const id = String(req.query.id || "").trim();
const page = Number(req.query.page || 1);
if (!id) throw new Error("Parameter id is required");
if (Number.isNaN(page)) throw new Error("Parameter page must be a number");
const apiUrl = getRelatedApiUrl(id, page);
const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8"));
const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
const data = videos.map((v: any) => ({
id: v.video_id,
title: v.title,
image: v.scr || v.thumb || null,
duration: v.duration || "None",
views: v.video_viewed || "0",
rating: v.rating || "0",
uploader: v.username || v.display_name || "",
link: `https://txxx.com/videos/${v.video_id}/${v.dir}/`,
video: `https://txxx.com/embed/${v.video_id}/`,
}));
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json({
success: true,
total_count: rawData.total_count || "0",
pages: rawData.pages || 1,
page,
data,
source: `https://txxx.com/videos/${id}/`,
} as ISearchVideoData);
} catch (err) {
const e = err as Error;
return res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -0,0 +1,58 @@
import { Request, Response } from "express";
import LustPress from "../../LustPress";
import { maybeError } from "../../utils/modifier";
import { logger } from "../../utils/logger";
const lust = new LustPress();
export async function randomTxxx(req: Request, res: Response) {
try {
const apiUrl =
"https://txxx.com/api/json/videos2/14400/str/most-popular/60/..1.all..day.json";
const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8"));
const videos = Array.isArray(rawData.videos)
? rawData.videos
: [];
if (videos.length === 0) {
throw new Error("No videos returned from upstream");
}
// Pick one random video
const v = videos[Math.floor(Math.random() * videos.length)];
const data = {
video_id: v.video_id,
title: v.title,
dir: v.dir,
duration: v.duration,
views: v.video_viewed,
rating: v.rating,
uploaded: v.post_date,
likes: v.likes,
dislikes: v.dislikes,
image: v.scr,
categories: v.categories ? v.categories.split(",") : [],
embed: `https://txxx.com/embed/${v.video_id}/`,
};
logger.info({
path: req.path,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent"),
});
return res.json({
success: true,
data,
source: apiUrl,
});
} catch (err) {
const e = err as Error;
return res.status(400).json(maybeError(false, e.message));
}
}

View File

@@ -0,0 +1,63 @@
import { Request, Response } from "express";
import LustPress from "../../LustPress";
import { maybeError } from "../../utils/modifier";
const lust = new LustPress();
export async function searchTxxx(req: Request, res: Response) {
try {
const key = String(req.query.key || "").trim();
const page = Number(req.query.page || 1);
if (!key) {
return res.json({
success: false,
error: "Parameter key is required",
});
}
if (Number.isNaN(page)) {
return res.json({
success: false,
error: "Parameter page must be a number",
});
}
const apiUrl =
`https://txxx.com/api/videos2.php` +
`?params=259200/str/relevance/60/search..${page}.all..` +
`&s=${encodeURIComponent(key)}`;
// Fetch from API directly
const buffer = await lust.fetchBody(apiUrl);
const rawData = JSON.parse(buffer.toString("utf-8"));
const videos = Array.isArray(rawData.videos) ? rawData.videos : [];
const data = videos.map((v: any) => ({
video_id: v.video_id,
title: v.title,
dir: v.dir,
duration: v.duration,
views: v.video_viewed,
rating: v.rating,
uploaded: v.post_date,
likes: v.likes,
dislikes: v.dislikes,
image: v.scr,
categories: v.categories ? v.categories.split(",") : [],
embed: `https://txxx.com/embed/${v.video_id}/`,
}));
return res.json({
success: true,
total_count: String(rawData.total_count ?? videos.length),
pages: Number(rawData.pages ?? 1),
page,
data,
});
} catch (err) {
const e = err as Error;
return res.json(maybeError(false, e.message));
}
}

View File

@@ -14,23 +14,23 @@ export async function getXhamster(req: Request, res: Response) {
* @apiName Get xhamster
* @apiGroup xhamster
* @apiDescription Get a xhamster video based on id
*
*
* @apiParam {String} id Video ID
*
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx
*
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
*
* axios.get("https://lust.scathach.id/xhamster/get?id=videos/horny-makima-tests-new-toy-and-cums-intensely-xhAa5wx")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
@@ -38,14 +38,14 @@ export async function getXhamster(req: Request, res: Response) {
* print(await resp.json())
*/
const url = `${c.XHAMSTER}/${id}`;
const url = `${c.XHAMSTER}/videos/${id}`;
const data = await scrapeContent(url);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
useragent: req.get("User-Agent"),
});
return res.json(data);
} catch (err) {

View File

@@ -10,51 +10,58 @@ const lust = new LustPress();
export async function randomXhamster(req: Request, res: Response) {
try {
/**
* @api {get} /xhamster/random Get random xhamster
* @api {get} /xhamster/random Get random xhamster video
* @apiName Get random xhamster
* @apiGroup xhamster
* @apiDescription Get a random xhamster video
*
* @apiDescription Get a random xhamster video from the list of newest videos.
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* HTTP/1.1 400 Bad Request
*
*
* @apiExample {curl} curl
* curl -i https://lust.scathach.id/xhamster/random
*
*
* @apiExample {js} JS/TS
* import axios from "axios"
*
*
* axios.get("https://lust.scathach.id/xhamster/random")
* .then(res => console.log(res.data))
* .catch(err => console.error(err))
*
*
* @apiExample {python} Python
* import aiohttp
* async with aiohttp.ClientSession() as session:
* async with session.get("https://lust.scathach.id/xhamster/random") as resp:
* print(await resp.json())
*/
const resolve = await lust.fetchBody(`${c.XHAMSTER}/newest`);
const $ = load(resolve);
const search = $("a.root-9d8b4.video-thumb-info__name.role-pop.with-dropdown")
.map((i, el) => $(el).attr("href"))
.get();
const search_ = search.map((el) => el.replace(c.XHAMSTER, ""));
const random = Math.floor(Math.random() * search_.length);
const url = c.XHAMSTER + search_[random];
const data = await scrapeContent(url);
const videoLinks = $("div.thumb-list__item[data-video-id]")
.map((_, el) => {
const href = $(el).find("a[data-role='thumb-link']").attr("href");
return href && href.includes("/videos/") ? href : null;
})
.get()
.filter(Boolean);
// Select a random video URL from the list
const randomIndex = Math.floor(Math.random() * videoLinks.length);
const randomUrl = videoLinks[randomIndex];
const data = await scrapeContent(randomUrl);
logger.info({
path: req.path,
query: req.query,
method: req.method,
ip: req.ip,
useragent: req.get("User-Agent")
useragent: req.get("User-Agent"),
});
return res.json(data);
} catch (err) {
const e = err as Error;

View File

@@ -3,13 +3,14 @@ export interface IVideoData {
data: {
title: string;
id: string;
image: string;
image?: string;
duration: string;
views: string;
rating: string;
rating?: string;
uploaded: string;
upvoted: string | null;
downvoted: string | null;
channel?: string;
models: string[];
tags: string[];
};
@@ -23,6 +24,19 @@ export interface ISearchVideoData {
source: string;
}
export interface ISearchItem {
link: string;
id: string;
title?: string;
image?: string;
duration?: string;
rating?: string;
views?: string;
uploader?: string;
video?: string;
}
export interface MaybeError {
message: string;
}

View File

@@ -2,6 +2,19 @@ import cors from "cors";
import { Router } from "express";
import { slow, limiter } from "../utils/limit-options";
// EPorner
import { getEporner } from "../controller/eporner/epornerGet";
import { searchEporner } from "../controller/eporner/epornerSearch";
import { relatedEporner } from "../controller/eporner/epornerGetRelated";
import { randomEporner } from "../controller/eporner/epornerRandom";
// TXXX
import { getTxxx } from "../controller/txxx/txxxGet";
import { searchTxxx } from "../controller/txxx/txxxSearch";
import { relatedTxxx } from "../controller/txxx/txxxGetRelated";
import { randomTxxx } from "../controller/txxx/txxxRandom";
// PornHub
import { getPornhub } from "../controller/pornhub/pornhubGet";
import { searchPornhub } from "../controller/pornhub/pornhubSearch";
@@ -65,6 +78,14 @@ function scrapeRoutes() {
router.get("/youporn/search", cors(), slow, limiter, searchYouporn);
router.get("/youporn/related", cors(), slow, limiter, relatedYouporn);
router.get("/youporn/random", cors(), slow, limiter, randomYouporn);
router.get("/eporner/get", cors(), slow, limiter, getEporner);
router.get("/eporner/search", cors(), slow, limiter, searchEporner);
router.get("/eporner/related", cors(), slow, limiter, relatedEporner);
router.get("/eporner/random", cors(), slow, limiter, randomEporner);
router.get("/txxx/get", cors(), slow, limiter, getTxxx);
router.get("/txxx/search", cors(), slow, limiter, searchTxxx);
router.get("/txxx/related", cors(), slow, limiter, relatedTxxx);
router.get("/txxx/random", cors(), slow, limiter, randomTxxx);
return router;
}

View File

@@ -0,0 +1,96 @@
import { load } from "cheerio";
import LustPress from "../../LustPress";
import { IVideoData } from "../../interfaces";
const lust = new LustPress();
function calculateRatingFromStrings(
upVote: string,
downVote: string
): number {
const up = parseInt(upVote.replace(/,/g, ""), 10) || 0;
const down = parseInt(downVote.replace(/,/g, ""), 10) || 0;
const total = up + down;
if (total === 0) return 0;
return up / total;
}
export async function scrapeContent(url: string) {
try {
const resolve = await lust.fetchBody(url);
const $ = load(resolve);
class EPorner {
link: string;
id: string;
title: string;
image: string;
duration: string;
views: string;
rating: string;
publish: string;
upVote: string;
downVote: string;
video: string;
tags: string[];
models: string[];
constructor() {
// https://www.eporner.com/video-ibvqvezXzcs/ivy-seduces-her-dad-s-friend/
this.link = $("link[rel='canonical']").attr("href") || "None";
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None";
this.duration = $("meta[property='og:duration']").attr("content") || "0";
this.views = $("#cinemaviews1").text().trim() || "0";
this.upVote = $(".likeup i").first().text().trim() || "0";
this.downVote = $(".likedown i").first().text().trim() || "0";
const jsonLdText = $('script[type="application/ld+json"]').first().html() || "{}";
const jsonLd = JSON.parse(jsonLdText);
const isoDuration = jsonLd.duration; // "PT00H8M12S"
this.publish = jsonLd.uploadDate || "None";
const ratingValue = calculateRatingFromStrings(this.upVote, this.downVote);
this.rating = (ratingValue * 100).toFixed(2);
const videoPart = this.link.split("/")[3] || "None";
const code = videoPart.replace("video-", "");
this.video = `https://www.eporner.com/embed/${code}/`;
this.tags = $("li.vit-category a")
.map((_, el) => $(el).text().trim()).get();
this.models = $("li.vit-pornstar a")
.map((_, el) => $(el).text().trim()).get();
}
}
const ep = new EPorner();
const data: IVideoData = {
success: true,
data: {
title: lust.removeHtmlTagWithoutSpace(ep.title),
id: ep.id,
image: ep.image,
duration: lust.secondToMinute(Number(ep.duration)),
views: ep.views,
rating: ep.rating,
uploaded: ep.publish,
upvoted: ep.upVote,
downvoted: ep.downVote,
models: ep.models,
tags: ep.tags
},
source: ep.link,
assets: [ep.video, ep.image]
};
return data;
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}

View File

@@ -0,0 +1,46 @@
import { load } from "cheerio";
import LustPress from "../../LustPress";
import { ISearchVideoData } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) {
const html = await lust.fetchBody(url);
const $ = load(html);
const data = $("#relateddiv .mb")
.map((i, el) => {
const link = $(el).find(".mbimg a").first().attr("href") || "";
const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
let id = "";
if (match) {
if (match[1] === "video-") {
id = `video-${match[2]}`;
} else {
// hd-porn
id = match[2];
}
}
return {
link: `https://www.eporner.com${link}`,
id,
title: $(el).find(".mbtit a").text().trim(),
image:
$(el).find("img").attr("data-src") || $(el).find("img").attr("src"),
duration: $(el).find(".mbtim").text().trim(),
rating: $(el).find(".mbrate").text().trim(),
views: $(el).find(".mbvie").text().trim(),
uploader: $(el).find(".mb-uploader a").text().trim(),
video: `https://www.eporner.com/embed/${id}`,
};
})
.get()
.filter((v) => v.id && v.image);
return {
success: true,
data: data as unknown as string[],
source: url,
} as ISearchVideoData;
}

View File

@@ -0,0 +1,70 @@
import { load } from "cheerio";
import LustPress from "../../LustPress";
import { ISearchItem } from "../../interfaces";
const lust = new LustPress();
export async function scrapeContent(url: string) {
try {
const res = await lust.fetchBody(url);
const $ = load(res);
class EPornerSearch {
data: ISearchItem[];
constructor() {
this.data = $("#vidresults .mb")
.map((i, el) => {
const link =
$(el).find(".mbimg a").first().attr("href") || "";
const match = link.match(/\/(video-|hd-porn\/)([^/]+)/);
let id = "";
if (match) {
if (match[1] === "video-") {
id = `video-${match[2]}`;
} else {
id = match[2];
}
}
const code = id.startsWith("video-")
? id.replace("video-", "")
: id;
return {
link: `https://www.eporner.com${link}`,
id,
title: $(el).find(".mbtit a").text().trim(),
image: $(el).find(".mbimg img").attr("src"),
duration: $(el).find(".mbtim").text().trim(),
rating: $(el).find(".mbrate").text().trim(),
views: $(el).find(".mbvie").text().trim(),
uploader: $(el)
.find(".mb-uploader a")
.text()
.trim(),
video: code
? `https://www.eporner.com/embed/${code}/`
: "None",
};
})
.get();
}
}
const ep = new EPornerSearch();
if (ep.data.length === 0) throw Error("No result found");
return {
success: true,
data: ep.data as ISearchItem[],
source: url,
};
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}

View File

@@ -9,7 +9,7 @@ export async function scrapeContent(url: string) {
const resolve = await lust.fetchBody(url);
const $ = load(resolve);
class PornHub {
class PornHub {
link: string;
id: string;
title: string;
@@ -29,28 +29,29 @@ export async function scrapeContent(url: string) {
this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None";
//get <meta property="video:duration" content="
this.duration = $("meta[property='video:duration']").attr("content") || "0";
this.duration =
$("meta[property='video:duration']").attr("content") || "0";
this.views = $("div.views > span.count").text() || "None";
this.rating = $("div.ratingPercent > span.percent").text() || "None";
this.videoInfo = $("div.videoInfo").text() || "None";
this.upVote = $("span.votesUp").attr("data-rating") || "None";
this.downVote = $("span.votesDown").attr("data-rating") || "None";
this.video = $("meta[property='og:video:url']").attr("content") || "None";
this.video =
$("meta[property='og:video:url']").attr("content") || "None";
this.tags = $("div.video-info-row")
.find("a")
.map((i, el) => {
return $(el).text();
}).get();
})
.get();
this.tags.shift();
this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el));
this.models = $("div.pornstarsWrapper.js-pornstarsWrapper")
.find("a")
.map((i, el) => {
return $(el).attr("data-mxptext");
}).get();
this.models = $("div.pornstarsWrapper.js-pornstarsWrapper a")
.map((i, el) => $(el).text().trim())
.get();
}
}
const ph = new PornHub();
const data: IVideoData = {
success: true,
@@ -65,14 +66,14 @@ export async function scrapeContent(url: string) {
upvoted: ph.upVote,
downvoted: ph.downVote,
models: ph.models,
tags: ph.tags.filter((el) => el !== "Suggest" && el !== " Suggest")
tags: ph.tags.filter((el) => el !== "Suggest" && el !== " Suggest"),
},
source: ph.link,
assets: [ph.video, ph.image]
assets: [ph.video, ph.image],
};
return data;
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}
}

View File

@@ -6,78 +6,106 @@ const lust = new LustPress();
export async function scrapeContent(url: string) {
try {
const resolve = await lust.fetchBody(url);
const $ = load(resolve);
const buffer = await lust.fetchBody(url);
const $ = load(buffer.toString("utf8"));
class Xhamster {
const raw = $("#initials-script").html();
const initials = raw
? JSON.parse(
raw.replace(/^window\.initials\s*=\s*/, "").replace(/;$/, ""),
)
: null;
class Xhamster {
link: string;
id: string;
title: string;
image: string;
duration: any;
duration: string;
views: string;
rating: string;
publish: string;
upVote: string;
downVote: string;
video: string;
publish: string;
tags: string[];
models: string[];
video: string;
constructor() {
this.link = $("link[rel='canonical']").attr("href") || "None";
this.id = this.link.split("/")[3] + "/" + this.link.split("/")[4] || "None";
this.id = this.link.split("/")[4] || "None";
this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None";
this.duration = $("script#initials-script").html() || "None";
//remove window.initials={ and };
this.duration = this.duration.replace("window.initials=", "");
this.duration = this.duration.replace(/;/g, "");
this.duration = JSON.parse(this.duration);
this.duration = this.duration.videoModel.duration || "None";
this.views = $("div.header-icons").find("span").first().text() || "None";
this.rating = $("div.header-icons").find("span").eq(1).text() || "None";
this.publish = $("div.entity-info-container__date").attr("data-tooltip") || "None";
this.upVote = $("div.rb-new__info").text().split("/")[0].trim() || "None";
this.downVote = $("div.rb-new__info").text().split("/")[1].trim() || "None";
this.video = "https://xheve2.com/embed/" + this.link.split("-").pop() || "None";
this.tags = $("a.video-tag")
.map((i, el) => {
return $(el).text();
}).get();
this.tags = this.tags.map((el) => lust.removeHtmlTagWithoutSpace(el));
this.models = $("a.video-tag")
.map((i, el) => {
return $(el).attr("href");
}
).get();
this.models = this.models.filter((el) => el.startsWith("https://xheve2.com/pornstars/"));
this.models = this.models.map((el) => el.replace("https://xheve2.com/pornstars/", ""));
this.image = $("meta[property='og:image']").attr("content") || "None";
// defaults
this.duration = "None";
this.views = "None";
const scripts = $("script")
.map((i, el) => $(el).html())
.get()
.filter(Boolean);
const videoScript = scripts.find(
(s) => s.includes('"videoModel"') && s.includes('"duration"'),
);
if (videoScript) {
const durMatch = videoScript.match(/"duration"\s*:\s*(\d+)/);
if (durMatch) this.duration = durMatch[1];
const viewMatch = videoScript.match(/"views"\s*:\s*(\d+)/);
if (viewMatch) this.views = viewMatch[1];
}
this.rating =
initials?.ratingComponent?.ratingModel?.value?.toString() || "None";
this.upVote =
initials?.ratingComponent?.ratingModel?.likes?.toString() || "None";
this.downVote =
initials?.ratingComponent?.ratingModel?.dislikes?.toString() ||
"None";
this.publish =
$("div.entity-info-container__date").attr("data-tooltip") || "None";
this.tags =
initials?.videoTagsComponent?.tags
?.filter((t: any) => t.isTag)
.map((t: any) => t.name) || [];
this.models =
initials?.videoTagsComponent?.tags
?.filter((t: any) => t.isPornstar)
.map((t: any) => t.name) || [];
const embedId = this.link.split("-").pop()?.replace("/", "");
this.video = embedId ? `https://xhamster.com/embed/${embedId}` : "None";
}
}
const xh = new Xhamster();
const data: IVideoData = {
return {
success: true,
data: {
title: lust.removeHtmlTagWithoutSpace(xh.title),
id: xh.id,
image: xh.image,
duration: lust.secondToMinute(Number(xh.duration)),
duration: xh.duration,
views: xh.views,
rating: xh.rating,
uploaded: xh.publish,
upvoted: xh.upVote,
downvoted: xh.downVote,
models: xh.models,
tags: xh.tags
tags: xh.tags,
},
source: xh.link,
assets: [xh.video, xh.image]
};
return data;
assets: [xh.video, xh.image],
} satisfies IVideoData;
} catch (err) {
const e = err as Error;
throw Error(e.message);
throw new Error(e.message);
}
}
}

View File

@@ -17,29 +17,32 @@ export async function scrapeContent(url: string) {
.map((i, el) => {
const views = $(el).text();
return views;
}).get();
})
.get();
const duration = $("span[data-role='video-duration']")
.map((i, el) => {
const duration = $(el).text();
return duration;
}).get();
})
.get();
this.search = $("a.video-thumb__image-container")
.map((i, el) => {
const link = $(el).attr("href");
return {
link: `${link}`,
id: link?.split("/")[3] + "/" + link?.split("/")[4],
id: link?.split("/")[4],
title: $(el).find("img").attr("alt"),
image: $(el).find("img").attr("src"),
duration: duration[i],
views: views[i],
video: `${c.XHAMSTER}/embed/${link?.split("-").pop()}`
video: `${c.XHAMSTER}/embed/${link?.split("-").pop()}`,
};
}).get();
})
.get();
}
}
const xh = new XhamsterSearch();
if (xh.search.length === 0) throw Error("No result found");
const data = xh.search as unknown as string[];
@@ -49,7 +52,6 @@ export async function scrapeContent(url: string) {
source: url,
};
return result;
} catch (err) {
const e = err as Error;
throw Error(e.message);

View File

@@ -9,7 +9,7 @@ export async function scrapeContent(url: string) {
const resolve = await lust.fetchBody(url);
const $ = load(resolve);
class YouPorn {
class YouPorn {
link: string;
id: string;
title: string;
@@ -25,27 +25,37 @@ export async function scrapeContent(url: string) {
models: string[];
constructor() {
this.link = $("link[rel='canonical']").attr("href") || "None";
this.id = this.link.replace("https://www.youporn.com/watch/", "") || "None";
this.id =
this.link.replace("https://www.youporn.com/watch/", "") || "None";
this.title = $("meta[property='og:title']").attr("content") || "None";
this.image = $("meta[property='og:image']").attr("content") || "None";
this.duration = $("meta[property='video:duration']").attr("content") || "0";
this.views = $("div.feature.infoValueBlock").find("div[data-value]").attr("data-value") || "0";
this.rating = $("div.feature").find("span").text().replace(/[^0-9.,%]/g, "") || "0";
this.publish = $("div.video-uploaded").find("span").text() || "None";
this.upVote = this.views;
this.duration =
$("meta[property='video:duration']").attr("content") || "0";
this.publish =
$("span.publishedDate").text().replace("Published on", "").trim() ||
"None";
this.upVote = "None";
this.downVote = "None";
this.video = `https://www.youporn.com/embed/${this.id}`;
this.tags = $("a[data-espnode='category_tag'], a[data-espnode='porntag_tag']")
.map((i, el) => {
return $(el).text();
}).get();
this.models = $("a[data-espnode='pornstar_tag']")
.map((i, el) => {
return $(el).text();
}).get();
this.views = $("span.tm_infoValue").first().text() || "0";
this.rating = $("span.tm_rating_percent").text() || "0";
this.tags = $(
"div.js_scrollableContent a.bubble-porntag, \
a[data-espnode='category_tag'], \
a[data-espnode='porntag_tag']",
)
.map((i, el) => $(el).text().trim())
.get()
.filter(Boolean);
this.models = $("#metaDataPornstarInfo a.tm_pornstar_link span")
.map((i, el) => $(el).text().replace(",", "").trim())
.get();
}
}
const yp = new YouPorn();
const data: IVideoData = {
success: true,
@@ -60,14 +70,14 @@ export async function scrapeContent(url: string) {
upvoted: yp.upVote,
downvoted: yp.downVote,
models: yp.models,
tags: yp.tags
tags: yp.tags,
},
source: yp.link,
assets: [yp.video, yp.image]
assets: [yp.video, yp.image],
};
return data;
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}
}

View File

@@ -11,31 +11,74 @@ export async function scrapeContent(url: string) {
const $ = load(res);
class YouPornSearch {
dur: string[];
links: string[];
ids: string[];
titles: string[];
images: string[];
durations: string[];
views: string[];
search: object[];
constructor() {
this.dur = $("div.video-duration").map((i, el) => {
return $(el).text();
}).get();
this.search = $("a[href^='/watch/']")
const cards = $("div.video-box.pc");
this.links = cards
.map((i, el) => {
return $(el).find("a.tm_video_link").attr("href");
})
.get();
this.ids = this.links.map((link) => link?.split("/")[2]);
this.titles = cards
.map((i, el) => {
return $(el).find("a.tm_video_title span").text().trim();
})
.get();
this.images = cards
.map((i, el) => {
return (
$(el).find("img.thumb-image").attr("data-src") ||
$(el).find("img.thumb-image").attr("src")
);
})
.get();
this.durations = cards
.map((i, el) => {
return $(el).find("div.tm_video_duration span").text().trim();
})
.get();
this.views = cards
.map((i, el) => {
return (
$(el)
.find(".view-rating-container .info-views")
.first()
.text()
.trim() || "None"
);
})
.get();
this.search = cards
.map((i, el) => {
const link = $(el).attr("href");
const id = `${link}`.split("/")[2] + "/" + `${link}`.split("/")[3];
const title = $(el).find("div.video-box-title").text();
const image = $(el).find("img").attr("data-thumbnail");
return {
link: `${c.YOUPORN}${link}`,
id: id,
title: lust.removeHtmlTagWithoutSpace(title),
image: image,
duration: this.dur[i],
views: "None",
video: `https://www.youporn.com/embed/${id}`,
link: `${c.YOUPORN}${this.links[i]}`,
id: this.ids[i],
title: lust.removeHtmlTagWithoutSpace(this.titles[i]),
image: this.images[i],
duration: this.durations[i],
views: this.views[i],
video: `https://www.youporn.com/embed/${this.ids[i]}`,
};
}).get();
})
.get();
}
}
const yp = new YouPornSearch();
if (yp.search.length === 0) throw Error("No result found");
const data = yp.search as unknown as string[];
@@ -45,9 +88,8 @@ export async function scrapeContent(url: string) {
source: url,
};
return result;
} catch (err) {
const e = err as Error;
throw Error(e.message);
}
}
}

View File

@@ -1,9 +1,11 @@
export default {
EPORNER: "https://www.eporner.com",
PORNHUB: "https://www.pornhub.com",
XNXX: "https://www.xnxx.com",
REDTUBE: "https://www.redtube.com",
XVIDEOS: "https://www.xvideos.com",
XHAMSTER: "https://xheve2.com",
XHAMSTER: "https://xhamster.com/",
YOUPORN: "https://www.youporn.com",
TXXX: "https://txxx.com/",
JAVHD: "https://javhd.today"
};