npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hotdreams-ts

v0.1.1

Published

TypeScript/Node.js port of hotdreams — scraping video metadata (title, duration, thumbnail, preview, views, rating...) from several adult video sites.

Readme

Table of contents

Installation

npm install hotdreams-ts

Requires Node.js 18+.

Quickstart

import { pornhub } from 'hotdreams-ts';

const result = await pornhub.searchPorn('lorena aquino');
console.log(result.videoCount, 'videos found');

for (const video of result.videos) {
  console.log(video.title, video.url);
}

Every site module exposes the same shape: searchPorn(query, pageLimit?, pageNumber?) returns a DataVideos object (.videos, .videoCount, .pageCount, ...), and most modules also expose getVideoEmbed(url) for fetching a single video's embed data and suggestions. Both are async and return a Promise.

Custom User-Agent and proxies

By default every site module ships with working headers. If you need to override the User-Agent (e.g. to look like a different browser) or route requests through a proxy (e.g. to avoid IP-based rate limiting), call these once, before or after importing the site modules you plan to use — they apply to every site the lib supports:

import { setUserAgent, setProxies, clearProxies, pornhub } from 'hotdreams-ts';

setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36');

setProxies({ http: 'http://user:pass@host:port', https: 'http://user:pass@host:port' });

// ... use the lib normally
await pornhub.searchPorn('lorena aquino');

// to stop using the proxy
clearProxies();

setUserAgent/setProxies retroactively update sessions already created, and also apply to any session created afterwards, so import order doesn't matter.

Custom cookies folder

Every site module persists the cookies it picks up (e.g. anti-bot/session cookies) to a small JSON file per site, and reuses them on the next call. By default those files are written to a hotdreams/cookies folder inside the OS temp dir, so nothing is left behind in your project directory. Call setCookiesDir() to store them somewhere else instead:

import { setCookiesDir, eporner } from 'hotdreams-ts';

setCookiesDir('/home/me/.hotdreams_cookies');

// ... use the lib normally
await eporner.searchPorn('lorena aquino');

Direct video URL

getDirectUrl(url) resolves a video page URL to the direct, playable/downloadable video file URL, by shelling out to the yt-dlp CLI. Unlike the Python package (which depends on the yt-dlp Python package), this needs the yt-dlp binary installed and on your PATH — it isn't an npm package, so npm install can't pull it in for you:

# pipx install yt-dlp, or brew install yt-dlp, or see
# https://github.com/yt-dlp/yt-dlp#installation
import { getDirectUrl } from 'hotdreams-ts';

await getDirectUrl('https://noodlemagazine.com/watch/-154109474_456276135');
// -> 'https://cdn77-pr.pvvstream.pro/videos/720/-154109474_456276135.mp4?...'

Supported hosts today: pornhub.com, xvideos.com, tnaflix.com, spankbang.com, playvids.com, noodlemagazine.com, eporner.com — these are the sites yt-dlp ships a dedicated extractor for. Calling it on any other host (pornone, ukdevilz, fpo.xxx, viralpornhub, nudes7) throws, since yt-dlp's generic extractor doesn't reliably resolve those.

Supported sites

| site | data video | preview | embed url video | suggested videos | |----------------|:----------:|:-------:|:----------------:|:-----------------:| | pornhub | ✅ | ✅ | ✅ | ✅ | | xvideos | ✅ | x | ✅ | x | | tnaflix | ✅ | ✅ | ✅ | ✅ | | noodlemagazine | ✅ | ✅ | ✅ | ✅ | | ukdevilz | ✅ | x | ✅ | | | pornone | ✅ | x | ✅ | | | eporner | ✅ | x | ✅ | ✅ | | fpo | ✅ | ✅ | ✅ | ✅ |

And sites from OnlyFans leaks, under only:

| site | data video | preview | |-------------------------|:----------:|:-------:| | only.nudes7 | ✅ | x | | only.viralpornhub | ✅ | ✅ |

import { only } from 'hotdreams-ts';

await only.nudes7.searchPorn('mia monroe');
await only.viralpornhub.searchPorn('mia monroe');

spankbang and playvids from the Python package aren't ported here: spankbang is blocked by Cloudflare (marked broken upstream too) and playvids never got embed/pagination support upstream.

API shape

interface VideoData {
  title: string | null;
  time: string | null;
  pageNumber: number | null;
  url: string | null;
  urlSource: string | null;      // the search/listing page this was scraped from
  thumbnail: string | null;
  siteName: string;
  preview?: string | null;
  stats?: string | null;
  index?: number | null;         // position within its listing page
  views?: string | null;
  viewsCount?: number | null;
  rating?: string | null;
  ratingValue?: number | null;
  uploadDate?: string | null;
  uploadDateSeconds?: number | null;
  duration?: number | null;
  durationSeconds?: number | null;
}

interface EmbedVideo {
  url: string | null;
  title: string | null;
  time: string | null;
  thumbnail: string | null;
  views: string | null;
  timePublished: string | null;
  siteName: string;
  suggestionsCount: number;
  suggestions: VideoData[] | null;
  rating?: string | null;
  durationSeconds?: number | string | null;
  uploadDate?: string | null;
  viewsCount?: number | string | null;
  likes?: number | string | null;
  person?: string | null;
  tags?: string | null;
}

interface DataVideos {
  siteName: string;
  urlBase: string;
  query: string;
  ping: number;               // seconds the search took
  videos: VideoData[];
  videosPerPage: number | null;
  pageCount: number | null;
  videoCount: number;
  urlSearch: string;
  success: boolean;
}

Differences from the Python package

This is a straight functional port, not a redesign, but a few things were adapted to fit Node/TypeScript conventions:

  • All field names are camelCase instead of snake_case (e.g. videoCount instead of len_videos, urlSource instead of url_font, index instead of indice).
  • sucess was corrected to success, videos_sugestions/len_videos_sugestions to suggestions/suggestionsCount.
  • HTTP + cookie-jar + proxy handling uses axios + tough-cookie instead of requests/mechanicalsoup/requests-html.
  • HTML parsing uses cheerio instead of BeautifulSoup.
  • getDirectUrl() shells out to the yt-dlp CLI instead of importing the yt-dlp Python package — install it separately, see Direct video URL.
  • A couple of quirky-but-intentional field mappings from the Python source were kept as-is (e.g. noodlemagazine's embed puts the raw views string into viewsCount rather than views, and ukdevilz's search results put view stats into .stats and duration into .duration instead of .views/.durationSeconds) — this mirrors the original per-site inconsistency rather than "fixing" it into something the Python examples in the upstream README no longer match.

License

BSD 3-Clause — see LICENSE.