├── .eslintrc.json ├── .coveralls.yml ├── .gitignore ├── .npmignore ├── .nycrc.json ├── src ├── configs │ ├── content.js │ ├── urls.js │ ├── headers.js │ └── search.js ├── pages │ ├── page.js │ ├── gallery.js │ ├── videos.js │ ├── model.js │ ├── search.js │ └── video.js ├── index.js └── utils │ ├── endpoints.js │ └── urlBuilder.js ├── tests ├── helpers │ ├── matching.js │ └── fixtures.js ├── pages │ ├── page.test.js │ ├── modelVideos.test.js │ ├── video.test.js │ ├── model.test.js │ ├── search.test.js │ └── videos.test.js └── fixtures │ └── loginPage.html ├── LICENSE ├── package.json ├── .circleci └── config.yml └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: JEvpz09FOkUEX8J8tOqtBbcwmdKucSpDF 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | .idea 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test.js 2 | .idea 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ 6 | tests/ 7 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": true, 4 | "reporter": ["html", "text", "text-lcov"] 5 | } 6 | -------------------------------------------------------------------------------- /src/configs/content.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | loginPageText: 'Content on Pornhub Premium can only be accessed by our members', 3 | errorPage: 'Error Page Not Found', 4 | noResults: [ 5 | 'No videos found', 6 | 'No Search Results', 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /tests/helpers/matching.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param object 4 | * @param array 5 | * @returns {*} 6 | */ 7 | const findMatchingKeys = (object = {}, array = []) => { 8 | const keys = Object.keys(object); 9 | 10 | return array.find((val) => keys.every((key) => val[key] === object[key])); 11 | }; 12 | 13 | module.exports = { 14 | findMatchingKeys, 15 | }; 16 | -------------------------------------------------------------------------------- /src/configs/urls.js: -------------------------------------------------------------------------------- 1 | const baseUrl = 'https://www.pornhubpremium.com'; 2 | const videoUrl = `${baseUrl}/video`; 3 | 4 | module.exports = { 5 | baseUrl, 6 | videoUrl, 7 | authUrl: `${baseUrl}/front/authenticate`, 8 | videoSearchUrl: `${videoUrl}/search`, 9 | modelSearchUrl: `${baseUrl}/pornstars/search`, 10 | incategories: `${videoUrl}/incategories`, 11 | }; 12 | -------------------------------------------------------------------------------- /tests/helpers/fixtures.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * Simple function to read in a fixture file. 6 | * @param fileName 7 | * @param encoding 8 | * @returns {*} 9 | */ 10 | const getFixture = (fileName, encoding = 'utf-8') => fs.readFileSync(path.resolve(__dirname, `../fixtures/${fileName}`), encoding); 11 | 12 | module.exports = { 13 | getFixture, 14 | }; 15 | -------------------------------------------------------------------------------- /src/configs/headers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accept: 'application/json, text/javascript, */*; q=0.01', 3 | 'accept-language': 'en-US,en;q=0.9', 4 | 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 5 | 'sec-ch-ua': '\' Not;A Brand\';v=\'99\', \'Google Chrome\';v=\'91\', \'Chromium\';v=\'91\'', 6 | 'sec-ch-ua-mobile': '?0', 7 | 'sec-fetch-dest': 'empty', 8 | 'sec-fetch-mode': 'cors', 9 | 'sec-fetch-site': 'same-origin', 10 | 'x-requested-with': 'XMLHttpRequest', 11 | }; 12 | -------------------------------------------------------------------------------- /src/pages/page.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | const content = require('../configs/content'); 3 | 4 | class Page { 5 | constructor(html) { 6 | this.html = html; 7 | this.dom = cheerio.load(html, { 8 | xml: { 9 | normalizeWhitespace: true, 10 | }, 11 | }); 12 | const domText = this.dom.text(); 13 | 14 | // Check auth. 15 | if (domText.includes(content.loginPageText)) { 16 | throw new Error('Auth failed'); 17 | } else if (domText.includes(content.errorPage)) { 18 | throw new Error('Page not found'); 19 | } 20 | } 21 | } 22 | 23 | module.exports = Page; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 notasmurf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pages/gallery.js: -------------------------------------------------------------------------------- 1 | const Page = require('./page'); 2 | const urls = require('../configs/urls'); 3 | const content = require('../configs/content'); 4 | 5 | class GalleryPage extends Page { 6 | getVideos() { 7 | const videoListItems = Array.from(this.dom('.pcVideoListItem')); 8 | 9 | const videos = videoListItems.map((videoListItem) => { 10 | const videoListElem = this.dom(videoListItem); 11 | 12 | const username = videoListElem.find('.usernameWrap').first().text().trim(); 13 | const duration = videoListElem.find('.duration').first().text().trim(); 14 | const titleElem = videoListElem.find('[title]'); 15 | const title = titleElem.attr('title').trim(); 16 | const url = titleElem.attr('href'); 17 | 18 | return { 19 | title, 20 | url: `${urls.baseUrl}${url}`, 21 | duration, 22 | username, 23 | }; 24 | }); 25 | 26 | return videos; 27 | } 28 | 29 | hasResults() { 30 | const domText = this.dom.text(); 31 | return !!content.noResults.find((noResultText) => domText.includes(noResultText)); 32 | } 33 | } 34 | 35 | module.exports = GalleryPage; 36 | -------------------------------------------------------------------------------- /src/pages/videos.js: -------------------------------------------------------------------------------- 1 | const GalleryPage = require('./gallery'); 2 | const urls = require('../configs/urls'); 3 | 4 | class VideosPage extends GalleryPage { 5 | constructor(html, options = {}) { 6 | super(html); 7 | 8 | this.options = options; 9 | } 10 | 11 | getPage() { 12 | return this.options.page || 1; 13 | } 14 | 15 | getCategories() { 16 | const categoryListItems = Array.from(this.dom('.checkHomepage')); 17 | 18 | const categories = categoryListItems 19 | .map((categoryListItem) => { 20 | const categoryListElem = this.dom(categoryListItem); 21 | const name = categoryListElem.find('.categoryName').first().text().trim(); 22 | let count = categoryListElem.find('.categoryNumber').first().text().trim(); 23 | count = parseInt(count.replace(',', ''), 10); 24 | const url = categoryListElem.find('a').first().attr('href'); 25 | 26 | return { 27 | name, 28 | count, 29 | url: `${urls.baseUrl}${url}`, 30 | }; 31 | }) 32 | .filter((category) => category.name); 33 | 34 | return categories; 35 | } 36 | } 37 | 38 | module.exports = VideosPage; 39 | -------------------------------------------------------------------------------- /src/pages/model.js: -------------------------------------------------------------------------------- 1 | const camelCase = require('lodash.camelcase'); 2 | const GalleryPage = require('./gallery'); 3 | const urls = require('../configs/urls'); 4 | 5 | class ModelPage extends GalleryPage { 6 | getDetailedInfo() { 7 | // @TODO: This function will be hyper sensitive to a DOM change. 8 | // @TODO: Try and find a better method for scraping this DOM. 9 | const detailElem = this.dom('.detailedInfo').first(); 10 | const propElems = Array.from(this.dom(detailElem).find('.infoPiece')); 11 | 12 | const details = propElems.reduce((acc, detail) => { 13 | const spans = Array.from(this.dom(detail).find('span')); 14 | const fieldElem = this.dom(spans[0]); 15 | const infoElem = this.dom(spans[1]); 16 | acc[camelCase(fieldElem.text().trim())] = infoElem.text().trim(); 17 | 18 | return acc; 19 | }, {}); 20 | 21 | return details; 22 | } 23 | 24 | getMoreVideosUrl() { 25 | const url = this.dom('#profileVideos a').attr('href'); 26 | return `${urls.baseUrl}${url}`; 27 | } 28 | 29 | getMorePremiumVideosUrl() { 30 | return `${this.getMoreVideosUrl()}?premium=1`; 31 | } 32 | } 33 | 34 | module.exports = ModelPage; 35 | -------------------------------------------------------------------------------- /tests/pages/page.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert'); 3 | const nock = require('nock'); 4 | const ph = require('../../src'); 5 | const fixtures = require('../helpers/fixtures'); 6 | 7 | describe('Page', () => { 8 | before(() => nock.disableNetConnect()); 9 | after(() => nock.enableNetConnect()); 10 | 11 | it('should error if a login page appears for a video request', async () => { 12 | nock('https://www.pornhubpremium.com') 13 | .get('/view_video.php') 14 | .query({ viewkey: 'ph60c3b0ef87832' }) 15 | .reply(200, fixtures.getFixture('loginPage.html')); 16 | 17 | const getPage = async () => ph.video('https://www.pornhubpremium.com/view_video.php?viewkey=ph60c3b0ef87832'); 18 | 19 | assert.rejects(getPage, Error); 20 | }); 21 | 22 | it('should error if the page was not found', async () => { 23 | nock('https://www.pornhubpremium.com') 24 | .get('/view_video.php') 25 | .query({ viewkey: 'ph60c3b0ef87832' }) 26 | .reply(200, fixtures.getFixture('notFoundPage.html')); 27 | 28 | const getPage = async () => ph.video('https://www.pornhubpremium.com/view_video.php?viewkey=ph60c3b0ef87832'); 29 | 30 | assert.rejects(getPage, Error); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/pages/search.js: -------------------------------------------------------------------------------- 1 | const GalleryPage = require('./gallery'); 2 | const urls = require('../configs/urls'); 3 | 4 | class SearchPage extends GalleryPage { 5 | constructor(html, options = {}) { 6 | super(html); 7 | 8 | this.options = options; 9 | } 10 | 11 | getPage() { 12 | return this.options.page || 1; 13 | } 14 | 15 | getModels() { 16 | const modelSearchResults = Array.from(this.dom('#pornstarsSearchResult .wrap')); 17 | 18 | const models = modelSearchResults.map((searchResult) => { 19 | const searchResultElem = this.dom(searchResult); 20 | const thumbnailWrapper = searchResultElem.find('.thumbnail-info-wrapper'); 21 | const thumbnailAnchor = thumbnailWrapper.find('a'); 22 | 23 | const name = thumbnailAnchor.text().trim(); 24 | const url = thumbnailAnchor.attr('href'); 25 | const rank = searchResultElem.find('.rank_number').text().trim(); 26 | const videos = thumbnailWrapper.find('.videosNumber').text().trim(); 27 | const views = thumbnailWrapper.find('.pstarViews').text().trim(); 28 | 29 | return { 30 | rank, 31 | name, 32 | videos, 33 | views, 34 | url: `${urls.baseUrl}${url}`, 35 | }; 36 | }); 37 | 38 | return models; 39 | } 40 | } 41 | 42 | module.exports = SearchPage; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notasmurf/pornhubpremium-api", 3 | "version": "0.0.0-alpha.8", 4 | "description": "Application Phappable Interface for PornHub Premium", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "eslint": "eslint ./src ./tests --ext .js", 8 | "eslint:fix": "npm run eslint -- --fix", 9 | "mocha": "mocha 'tests/**/*.test.js'", 10 | "nyc": "nyc npm run mocha", 11 | "coverage": "npm run nyc | coveralls", 12 | "test": "npm run eslint && npm run coverage", 13 | "start": "node src/index" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "cheerio": "^1.0.0-rc.10", 19 | "lodash.camelcase": "^4.3.0", 20 | "lodash.kebabcase": "^4.1.1", 21 | "nock": "^13.1.1", 22 | "node-fetch": "^2.6.1", 23 | "query-string": "^7.0.1" 24 | }, 25 | "devDependencies": { 26 | "coveralls": "^3.1.1", 27 | "eslint": "^7.30.0", 28 | "eslint-config-airbnb": "^18.2.1", 29 | "eslint-plugin-import": "^2.23.4", 30 | "eslint-plugin-jsx-a11y": "^6.4.1", 31 | "eslint-plugin-react": "^7.24.0", 32 | "mocha": "^9.0.2", 33 | "mocha-lcov-reporter": "^1.3.0", 34 | "nyc": "^15.1.0" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/notasmurf/pornhubpremium-api.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/notasmurf/pornhubpremium-api/issues" 42 | }, 43 | "homepage": "https://github.com/notasmurf/pornhubpremium-api#readme" 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/video.js: -------------------------------------------------------------------------------- 1 | const Page = require('./page'); 2 | const urls = require('../configs/urls'); 3 | 4 | class VideoPage extends Page { 5 | getDownloads() { 6 | const downloadButtons = Array.from(this.dom('.downloadBtn')); 7 | 8 | const downloads = downloadButtons.map((downloadButton) => { 9 | const downloadButtonElem = this.dom(downloadButton); 10 | const definition = downloadButtonElem.text().trim(); 11 | const url = downloadButtonElem.attr('href'); 12 | 13 | return { 14 | definition, 15 | url, 16 | }; 17 | }); 18 | 19 | return downloads; 20 | } 21 | 22 | getModels() { 23 | const modelButtons = Array.from(this.dom('.pstar-list-btn')); 24 | 25 | const models = modelButtons.map((modelButton) => { 26 | const modelButtonElem = this.dom(modelButton); 27 | const name = modelButtonElem.attr('data-mxptext'); 28 | const url = modelButtonElem.attr('href'); 29 | 30 | return { 31 | name, 32 | url: `${urls.baseUrl}${url}`, 33 | }; 34 | }); 35 | 36 | return models; 37 | } 38 | 39 | getCategories() { 40 | const categoryItems = Array.from(this.dom('.categoriesWrapper .item')); 41 | 42 | const categories = categoryItems.map((categoryItem) => { 43 | const categoryItemElem = this.dom(categoryItem); 44 | const url = categoryItemElem.attr('href'); 45 | const name = categoryItemElem.text(); 46 | 47 | return { 48 | url: `${urls.baseUrl}${url}`, 49 | name, 50 | }; 51 | }); 52 | 53 | return categories; 54 | } 55 | } 56 | 57 | module.exports = VideoPage; 58 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | orbs: 6 | # The Node.js orb contains a set of prepackaged CircleCI configuration you can utilize 7 | # Orbs reduce the amount of configuration required for common tasks. 8 | # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/node 9 | node: circleci/node@4.1 10 | coveralls: coveralls/coveralls@1.0.4 11 | 12 | 13 | jobs: 14 | # Below is the definition of your job to build and test your app, you can rename and customize it as you want. 15 | build-and-test: 16 | # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/ 17 | # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 18 | # A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/node 19 | docker: 20 | - image: cimg/node:15.1 21 | # Then run your tests! 22 | # CircleCI will report the results back to your VCS provider. 23 | steps: 24 | # Checkout the code as the first step. 25 | - checkout 26 | # Next, the node orb's install-packages step will install the dependencies from a package.json. 27 | # The orb install-packages step will also automatically cache them for faster future runs. 28 | - node/install-packages 29 | # If you are using yarn instead npm, remove the line above and uncomment the two lines below. 30 | # - node/install-packages: 31 | # pkg-manager: yarn 32 | - run: 33 | name: Run tests 34 | command: npm run coverage 35 | 36 | workflows: 37 | # Below is the definition of your workflow. 38 | # Inside the workflow, you provide the jobs you want to run, e.g this workflow runs the build-and-test job above. 39 | # CircleCI will run this workflow on every commit. 40 | # For more details on extending your workflow, see the configuration docs: https://circleci.com/docs/2.0/configuration-reference/#workflows 41 | sample: 42 | jobs: 43 | - build-and-test 44 | # For running simple node tests, you could optionally use the node/test job from the orb to replicate and replace the job above in fewer lines. 45 | # - node/test 46 | -------------------------------------------------------------------------------- /tests/pages/modelVideos.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert'); 3 | const nock = require('nock'); 4 | const ph = require('../../src'); 5 | const fixtures = require('../helpers/fixtures'); 6 | const matching = require('../helpers/matching'); 7 | 8 | describe('Videos Page', () => { 9 | before(() => nock.disableNetConnect()); 10 | after(() => nock.enableNetConnect()); 11 | 12 | it('should use the provided cookie in the request', async () => { 13 | const cookie = 'tinycookie;'; 14 | nock('https://www.pornhubpremium.com') 15 | .get('/pornstar/britney-amber') 16 | .matchHeader('cookie', cookie) 17 | .reply(200, ''); 18 | 19 | await ph.authenticate({ cookie }); 20 | await ph.modelVideos('https://www.pornhubpremium.com/pornstar/britney-amber'); 21 | }); 22 | 23 | it('should correctly parameterize the page number', async () => { 24 | const expected = { page: '2' }; 25 | nock('https://www.pornhubpremium.com') 26 | .get('/video') 27 | .query(expected) 28 | .reply(200, ''); 29 | 30 | await ph.videos({ page: 2 }); 31 | }); 32 | 33 | it('should return a page number', async () => { 34 | nock('https://www.pornhubpremium.com') 35 | .get('/pornstar/britney-amber') 36 | .query({ page: '2' }) 37 | .reply(200, ''); 38 | 39 | const modelVideosPage = await ph.modelVideos('https://www.pornhubpremium.com/pornstar/britney-amber', { page: 2 }); 40 | const pageNumber = modelVideosPage.getPage(); 41 | 42 | assert.equal(pageNumber, 2); 43 | }); 44 | 45 | it('should return all videos on the pages', async () => { 46 | const expected = { 47 | title: 'Stepmom Britney Amber Discipline Her Naughty Stepdaughter Katya Rodriguez', 48 | url: 'https://www.pornhubpremium.com/view_video.php?viewkey=ph60c34a288632f', 49 | duration: '21:56', 50 | username: 'Broken MILF', 51 | }; 52 | nock('https://www.pornhubpremium.com') 53 | .filteringPath(() => '/pornstar/britney-amber') 54 | .get('/pornstar/britney-amber') 55 | .reply(200, fixtures.getFixture('validModelVideosResult.html')); 56 | 57 | const modelVideosPage = await ph.modelVideos('https://www.pornhubpremium.com/pornstar/britney-amber'); 58 | const videos = modelVideosPage.getVideos(); 59 | 60 | const hasExpected = matching.findMatchingKeys(expected, videos); 61 | assert.ok(hasExpected); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const endpoints = require('./utils/endpoints'); 2 | const VideosPage = require('./pages/videos'); 3 | const VideoPage = require('./pages/video'); 4 | const SearchPage = require('./pages/search'); 5 | const ModelPage = require('./pages/model'); 6 | 7 | let cookie; 8 | 9 | /** 10 | * 11 | * @param credentials 12 | * @returns {Promise} 13 | * @private 14 | */ 15 | const authenticate = async (credentials = {}) => { 16 | const { username, password, cookie: authCookie } = credentials; 17 | 18 | if (!authCookie) { 19 | // @TODO: Make this work. 20 | const response = await endpoints.fetchAuth(username, password); 21 | cookie = response.headers.get('set-cookie'); 22 | } else { 23 | cookie = authCookie; 24 | } 25 | }; 26 | 27 | /** 28 | * 29 | * @param options 30 | * @returns {Promise} 31 | * @private 32 | */ 33 | const videos = async (options = {}) => { 34 | const response = await endpoints.fetchVideos(options, cookie); 35 | 36 | const html = await response.text(); 37 | const videosPage = new VideosPage(html, options); 38 | 39 | return videosPage; 40 | }; 41 | 42 | /** 43 | * 44 | * @param url 45 | * @returns {Promise} 46 | * @private 47 | */ 48 | const video = async (url) => { 49 | const response = await endpoints.fetchVideo(url, cookie); 50 | 51 | const html = await response.text(); 52 | const videoPage = new VideoPage(html); 53 | 54 | return videoPage; 55 | }; 56 | 57 | /** 58 | * 59 | * @param text 60 | * @param type 61 | * @param options 62 | * @returns {Promise} 63 | */ 64 | const search = async (text = '', type = 'videos', options = {}) => { 65 | const response = await endpoints.fetchSearch(text, type, options, cookie); 66 | 67 | const html = await response.text(); 68 | const searchPage = new SearchPage(html, options); 69 | 70 | return searchPage; 71 | }; 72 | 73 | /** 74 | * 75 | * @param url 76 | * @returns {Promise} 77 | */ 78 | const model = async (url) => { 79 | const response = await endpoints.fetchModel(url, cookie); 80 | 81 | const html = await response.text(); 82 | const modelPage = new ModelPage(html); 83 | 84 | return modelPage; 85 | }; 86 | 87 | /** 88 | * 89 | * @param url 90 | * @param options 91 | * @returns {Promise} 92 | */ 93 | const modelVideos = async (url, options = {}) => { 94 | const response = await endpoints.fetchModelVideos(url, options, cookie); 95 | 96 | const html = await response.text(); 97 | const videosPage = new VideosPage(html, options); 98 | 99 | return videosPage; 100 | }; 101 | 102 | module.exports = { 103 | authenticate, 104 | videos, 105 | video, 106 | search, 107 | model, 108 | modelVideos, 109 | }; 110 | -------------------------------------------------------------------------------- /tests/pages/video.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert'); 3 | const nock = require('nock'); 4 | const ph = require('../../src'); 5 | const fixtures = require('../helpers/fixtures'); 6 | const matching = require('../helpers/matching'); 7 | 8 | describe('Video Page', () => { 9 | before(() => nock.disableNetConnect()); 10 | after(() => nock.enableNetConnect()); 11 | 12 | it('should use the provided cookie in the request', async () => { 13 | const cookie = 'tinycookie;'; 14 | nock('https://www.pornhubpremium.com') 15 | .get('/view_video.php') 16 | .query({ viewkey: 'ph60c3b0ef87832' }) 17 | .matchHeader('cookie', cookie) 18 | .reply(200, ''); 19 | 20 | await ph.authenticate({ cookie }); 21 | await ph.video('https://www.pornhubpremium.com/view_video.php?viewkey=ph60c3b0ef87832'); 22 | }); 23 | 24 | it('should return an array of models', async () => { 25 | const expected = [{ 26 | name: 'Peter King', 27 | url: 'https://www.pornhubpremium.com/pornstar/peter-king', 28 | }]; 29 | nock('https://www.pornhubpremium.com') 30 | .get('/view_video.php') 31 | .query({ viewkey: 'ph60c3b0ef87832' }) 32 | .reply(200, fixtures.getFixture('validVideoResult.html')); 33 | 34 | const videoPage = await ph.video('https://www.pornhubpremium.com/view_video.php?viewkey=ph60c3b0ef87832'); 35 | const models = videoPage.getModels(); 36 | 37 | assert.deepEqual(models, expected); 38 | }); 39 | 40 | it('should return a list of download links', async () => { 41 | const expected = { 42 | definition: 'HD 1440p', 43 | url: 'https://ed.phncdn.com/videos/202107/02/390581561/1440P_6000K_390581561.mp4?validfrom=1625571027&validto=1625578227&rate=50000k&burst=50000k&ip=71.241.248.52&ipa=71.241.248.52&hash=oW6FEmjz4V4d4Be0vNOj4ejT8Wk%3D', 44 | }; 45 | nock('https://www.pornhubpremium.com') 46 | .get('/view_video.php') 47 | .query({ viewkey: 'ph60c3b0ef87832' }) 48 | .reply(200, fixtures.getFixture('validVideoResult.html')); 49 | 50 | const videoPage = await ph.video('https://www.pornhubpremium.com/view_video.php?viewkey=ph60c3b0ef87832'); 51 | const downloads = videoPage.getDownloads(); 52 | const hasExpected = matching.findMatchingKeys(expected, downloads); 53 | 54 | assert.ok(hasExpected); 55 | }); 56 | 57 | it('should return an array of categories associated to the video', async () => { 58 | const expected = { url: 'https://www.pornhubpremium.com/video?c=4', name: 'Big Ass' }; 59 | nock('https://www.pornhubpremium.com') 60 | .get('/view_video.php') 61 | .query({ viewkey: 'ph60c3b0ef87832' }) 62 | .reply(200, fixtures.getFixture('validVideoResult.html')); 63 | 64 | const videoPage = await ph.video('https://www.pornhubpremium.com/view_video.php?viewkey=ph60c3b0ef87832'); 65 | const categories = videoPage.getCategories(); 66 | const hasExpected = matching.findMatchingKeys(expected, categories); 67 | 68 | assert.ok(hasExpected); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/utils/endpoints.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const urls = require('../configs/urls'); 3 | const headers = require('../configs/headers'); 4 | const urlBuilder = require('./urlBuilder'); 5 | 6 | /** 7 | * 8 | * @param username 9 | * @param password 10 | * @returns {Promise<*>} 11 | */ 12 | const fetchAuth = async (username, password) => fetch(urls.authUrl, { 13 | headers, 14 | referrerPolicy: 'strict-origin-when-cross-origin', 15 | body: `username=${username}&password=${password}&remember_me=on&token=MTYyNTQyMDcwNb5XIi8iJdfI4nufH181JoGiF_SGbxS1ViuPgoFR5NkyppKnRiv6Aje9oYvSesEUEamrXgxmtEnbe-yzWOrkN6o.&redirect=&from=pc_premium_login&segment=straight`, 16 | method: 'POST', 17 | mode: 'cors', 18 | }); 19 | 20 | /** 21 | * 22 | * @param options 23 | * @param cookie 24 | * @returns {Promise} 25 | */ 26 | const fetchVideos = async (options, cookie) => fetch(urlBuilder.buildVideosUrl(options), 27 | { 28 | headers: { 29 | ...headers, 30 | cookie, 31 | }, 32 | referrerPolicy: 'strict-origin-when-cross-origin', 33 | body: null, 34 | method: 'GET', 35 | mode: 'cors', 36 | }); 37 | 38 | /** 39 | * 40 | * @param url 41 | * @param cookie 42 | * @returns {Promise} 43 | */ 44 | const fetchVideo = async (url, cookie) => fetch(url, { 45 | headers: { 46 | ...headers, 47 | cookie, 48 | }, 49 | referrerPolicy: 'strict-origin-when-cross-origin', 50 | body: null, 51 | method: 'GET', 52 | mode: 'cors', 53 | }); 54 | 55 | /** 56 | * 57 | * @param search 58 | * @param type 59 | * @param options 60 | * @param cookie 61 | * @returns {Promise} 62 | */ 63 | const fetchSearch = async (search, type, options, cookie) => fetch( 64 | urlBuilder.buildSearchUrl(search, type, options), { 65 | headers: { 66 | ...headers, 67 | cookie, 68 | }, 69 | referrerPolicy: 'strict-origin-when-cross-origin', 70 | body: null, 71 | method: 'GET', 72 | mode: 'cors', 73 | }, 74 | ); 75 | 76 | /** 77 | * 78 | * @param url 79 | * @param cookie 80 | * @returns {Promise} 81 | */ 82 | const fetchModel = async (url, cookie) => fetch(url, { 83 | headers: { 84 | ...headers, 85 | cookie, 86 | }, 87 | referrerPolicy: 'strict-origin-when-cross-origin', 88 | body: null, 89 | method: 'GET', 90 | mode: 'cors', 91 | }); 92 | 93 | /** 94 | * 95 | * @param url 96 | * @param options 97 | * @param cookie 98 | * @returns {Promise} 99 | */ 100 | const fetchModelVideos = async (url, options, cookie) => fetch( 101 | urlBuilder.buildModelVideosUrl(url, options), { 102 | headers: { 103 | ...headers, 104 | cookie, 105 | }, 106 | referrerPolicy: 'strict-origin-when-cross-origin', 107 | body: null, 108 | method: 'GET', 109 | mode: 'cors', 110 | }, 111 | ); 112 | 113 | module.exports = { 114 | fetchAuth, 115 | fetchVideos, 116 | fetchVideo, 117 | fetchSearch, 118 | fetchModel, 119 | fetchModelVideos, 120 | }; 121 | -------------------------------------------------------------------------------- /tests/pages/model.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert'); 3 | const nock = require('nock'); 4 | const ph = require('../../src'); 5 | const fixtures = require('../helpers/fixtures'); 6 | 7 | describe('Models Page', () => { 8 | before(() => nock.disableNetConnect()); 9 | after(() => nock.enableNetConnect()); 10 | 11 | it('should use the provided cookie in the request', async () => { 12 | const cookie = 'tinycookie;'; 13 | nock('https://www.pornhubpremium.com') 14 | .get('/pornstar/britney-amber') 15 | .matchHeader('cookie', cookie) 16 | .reply(200, ''); 17 | 18 | await ph.authenticate({ cookie }); 19 | await ph.model('https://www.pornhubpremium.com/pornstar/britney-amber'); 20 | }); 21 | 22 | it('should return model\'s details', async () => { 23 | const expected = { 24 | relationshipStatus: 'Single', 25 | interestedIn: 'Guys and Girls', 26 | cityAndCountry: 'Southern California, US', 27 | pornstarProfileViews: '35,568,683', 28 | careerStatus: 'Active', 29 | careerStartAndEnd: '2008 to Present', 30 | gender: 'Female', 31 | birthPlace: 'Banning, California, United States of America', 32 | starSign: 'Scorpio', 33 | measurements: '36D-24-34', 34 | height: '5 ft 5 in (165 cm)', 35 | weight: '115 lbs (52 kg)', 36 | ethnicity: 'White', 37 | hairColor: 'Blonde', 38 | fakeBoobs: 'Yes', 39 | tattoos: 'Yes', 40 | piercings: 'Yes', 41 | interestsAndHobbies: 'Archery and Bowhunting', 42 | hometown: 'Southern California', 43 | profileViews: '21,998,089', 44 | videosWatched: '51', 45 | }; 46 | nock('https://www.pornhubpremium.com') 47 | .get('/pornstar/britney-amber') 48 | .reply(200, fixtures.getFixture('validModelResult.html')); 49 | 50 | const model = await ph.model('https://www.pornhubpremium.com/pornstar/britney-amber'); 51 | const details = model.getDetailedInfo(); 52 | 53 | assert.deepEqual(expected, details); 54 | }); 55 | 56 | it('should return a URL for videos', async () => { 57 | const expected = 'https://www.pornhubpremium.com/pornstar/britney-amber/videos'; 58 | nock('https://www.pornhubpremium.com') 59 | .get('/pornstar/britney-amber') 60 | .reply(200, fixtures.getFixture('validModelResult.html')); 61 | 62 | const model = await ph.model('https://www.pornhubpremium.com/pornstar/britney-amber'); 63 | const url = model.getMoreVideosUrl(); 64 | 65 | assert.equal(expected, url); 66 | }); 67 | 68 | it('should return a URL for premium videos', async () => { 69 | const expected = 'https://www.pornhubpremium.com/pornstar/britney-amber/videos?premium=1'; 70 | nock('https://www.pornhubpremium.com') 71 | .get('/pornstar/britney-amber') 72 | .reply(200, fixtures.getFixture('validModelResult.html')); 73 | 74 | const model = await ph.model('https://www.pornhubpremium.com/pornstar/britney-amber'); 75 | const url = model.getMorePremiumVideosUrl(); 76 | 77 | assert.equal(expected, url); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/pages/search.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert'); 3 | const nock = require('nock'); 4 | const ph = require('../../src'); 5 | const fixtures = require('../helpers/fixtures'); 6 | const matching = require('../helpers/matching'); 7 | 8 | describe('Search Page', () => { 9 | before(() => nock.disableNetConnect()); 10 | after(() => nock.enableNetConnect()); 11 | 12 | it('should use the provided cookie in the request', async () => { 13 | const cookie = 'tinycookie;'; 14 | nock('https://www.pornhubpremium.com') 15 | .filteringPath(() => '/pornstars/search') 16 | .get('/pornstars/search') 17 | .matchHeader('cookie', cookie) 18 | .reply(200, ''); 19 | 20 | await ph.authenticate({ cookie }); 21 | await ph.search('Abby', 'pornstars'); 22 | }); 23 | 24 | it('should correctly parameterize the search', async () => { 25 | const expected = { search: 'Abby' }; 26 | nock('https://www.pornhubpremium.com') 27 | .get('/pornstars/search') 28 | .query(expected) 29 | .reply(200, ''); 30 | 31 | await ph.search('Abby', 'pornstars'); 32 | }); 33 | 34 | it('should correctly parameterize the page number', async () => { 35 | const expected = { search: 'Abby', page: '2' }; 36 | nock('https://www.pornhubpremium.com') 37 | .get('/pornstars/search') 38 | .query(expected) 39 | .reply(200, ''); 40 | 41 | await ph.search('Abby', 'pornstars', { page: 2 }); 42 | }); 43 | 44 | it('should return a page number', async () => { 45 | nock('https://www.pornhubpremium.com') 46 | .get('/pornstars/search') 47 | .query({ search: 'Abby', page: '2' }) 48 | .reply(200, ''); 49 | 50 | const results = await ph.search('Abby', 'pornstars', { page: 2 }); 51 | const pageNumber = results.getPage(); 52 | 53 | assert.equal(pageNumber, 2); 54 | }); 55 | 56 | it('should default the page number to 1', async () => { 57 | nock('https://www.pornhubpremium.com') 58 | .get('/pornstars/search') 59 | .query({ search: 'Abby' }) 60 | .reply(200, ''); 61 | 62 | const results = await ph.search('Abby', 'pornstars'); 63 | const pageNumber = results.getPage(); 64 | 65 | assert.equal(pageNumber, 1); 66 | }); 67 | 68 | it('should return a list of models for a models search', async () => { 69 | const expected = { 70 | rank: '8041', 71 | name: 'Abby Lane', 72 | videos: '12 Videos', 73 | views: '382K views', 74 | url: 'https://www.pornhubpremium.com/pornstar/abby-lane', 75 | }; 76 | nock('https://www.pornhubpremium.com') 77 | .filteringPath(() => '/pornstars/search') 78 | .get('/pornstars/search') 79 | .reply(200, fixtures.getFixture('validSearchModelResult.html')); 80 | 81 | const results = await ph.search('Abby', 'pornstars', { page: 2 }); 82 | const models = results.getModels(); 83 | 84 | const hasExpected = matching.findMatchingKeys(expected, models); 85 | assert.ok(hasExpected); 86 | }); 87 | 88 | it('should return a list of videos for a videos search', async () => { 89 | const expected = { 90 | title: 'My Friend‘s Mom By Cory Chase and Codey Steele', 91 | url: 'https://www.pornhubpremium.com/view_video.php?viewkey=ph60e0b1cde0307', 92 | duration: '30:30', 93 | username: 'Dr.K In LA', 94 | }; 95 | nock('https://www.pornhubpremium.com') 96 | .filteringPath(() => '/pornstars/search') 97 | .get('/pornstars/search') 98 | .reply(200, fixtures.getFixture('validSearchModelResult.html')); 99 | 100 | const results = await ph.search('Abby', 'videos', { page: 2 }); 101 | const videos = results.getVideos(); 102 | 103 | const hasExpected = matching.findMatchingKeys(expected, videos); 104 | assert.ok(hasExpected); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/configs/search.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | categories: { 3 | popularWithWomen: '/popularwithwomen', 4 | verifiedAmateurs: '/video?c=138', 5 | verifiedModels: '/video?c=139', 6 | virtualReality: '/vr', 7 | '60Fps': '/video?c=105', 8 | amateur: '/video?c=3', 9 | anal: '/video?c=35', 10 | arab: '/video?c=98', 11 | asian: '/video?c=1', 12 | babe: '/categories/babe', 13 | babysitter: '/video?c=89', 14 | bbw: '/video?c=6', 15 | behindTheScenes: '/video?c=141', 16 | bigAss: '/video?c=4', 17 | bigDick: '/video?c=7', 18 | bigTits: '/video?c=8', 19 | bisexualMale: '/video?c=76', 20 | blonde: '/video?c=9', 21 | blowjob: '/video?c=13', 22 | bondage: '/video?c=10', 23 | brazilian: '/video?c=102', 24 | british: '/video?c=96', 25 | brunette: '/video?c=11', 26 | bukkake: '/video?c=14', 27 | cartoon: '/video?c=86', 28 | casting: '/video?c=90', 29 | celebrity: '/video?c=12', 30 | closedCaptions: '/video?c=732', 31 | college: '/categories/college', 32 | compilation: '/video?c=57', 33 | cosplay: '/video?c=241', 34 | creampie: '/video?c=15', 35 | cuckold: '/video?c=242', 36 | cumshot: '/video?c=16', 37 | czech: '/video?c=100', 38 | describedVideo: '/described-video', 39 | doublePenetration: '/video?c=72', 40 | ebony: '/video?c=17', 41 | euro: '/video?c=55', 42 | exclusive: '/video?c=115', 43 | feet: '/video?c=93', 44 | femaleOrgasm: '/video?c=502', 45 | fetish: '/video?c=18', 46 | fingering: '/video?c=592', 47 | fisting: '/video?c=19', 48 | french: '/video?c=94', 49 | funny: '/video?c=32', 50 | gangbang: '/video?c=80', 51 | gay: '/gayporn', 52 | german: '/video?c=95', 53 | handjob: '/video?c=20', 54 | hardcore: '/video?c=21', 55 | hdPorn: '/hd', 56 | hentai: '/categories/hentai', 57 | indian: '/video?c=101', 58 | interactive: '/interactive', 59 | interracial: '/video?c=25', 60 | italian: '/video?c=97', 61 | japanese: '/video?c=111', 62 | korean: '/video?c=103', 63 | latina: '/video?c=26', 64 | lesbian: '/video?c=27', 65 | massage: '/video?c=78', 66 | masturbation: '/video?c=22', 67 | mature: '/video?c=28', 68 | milf: '/video?c=29', 69 | muscularMen: '/video?c=512', 70 | music: '/video?c=121', 71 | oldYoung: '/video?c=181', 72 | orgy: '/video?c=2', 73 | parody: '/video?c=201', 74 | party: '/video?c=53', 75 | pissing: '/video?c=211', 76 | pornstar: '/categories/pornstar', 77 | pov: '/video?c=41', 78 | public: '/video?c=24', 79 | pussyLicking: '/video?c=131', 80 | reality: '/video?c=31', 81 | redHead: '/video?c=42', 82 | rolePlay: '/video?c=81', 83 | romantic: '/video?c=522', 84 | roughSex: '/video?c=67', 85 | russian: '/video?c=99', 86 | school: '/video?c=88', 87 | sfw: '/sfw', 88 | smallTits: '/video?c=59', 89 | smoking: '/video?c=91', 90 | soloFemale: '/video?c=492', 91 | soloMale: '/video?c=92', 92 | squirt: '/video?c=69', 93 | stepFantasy: '/video?c=444', 94 | strapOn: '/video?c=542', 95 | striptease: '/video?c=33', 96 | tattooedWomen: '/video?c=562', 97 | teen18: '/categories/teen', 98 | threesome: '/video?c=65', 99 | toys: '/video?c=23', 100 | transgender: '/transgender', 101 | verifiedCouples: '/video?c=482', 102 | vintage: '/video?c=43', 103 | webcam: '/video?c=61', 104 | }, 105 | rankings: { 106 | mostViewed: 'mv', 107 | topRated: 'tr', 108 | hottest: 'ht', 109 | longest: 'lg', 110 | newest: 'cm', 111 | }, 112 | ranges: { 113 | daily: 't', 114 | weekly: 'w', 115 | monthly: 'm', 116 | yearly: 'y', 117 | allTime: 'a', 118 | }, 119 | rangedRankings: ['mostViewed', 'topRated'], 120 | quality: { 121 | '4k': '4', 122 | '1440p': '3', 123 | '1080p': '2', 124 | all: 1, 125 | }, 126 | video: { 127 | premiumOnly: 'premium', 128 | downloadOnly: 'download', 129 | }, 130 | keys: { 131 | rankings: 'o', 132 | quality: 'hd', 133 | production: 'p', 134 | ranges: 't', 135 | }, 136 | duration: { 137 | max: 'max_duration', 138 | min: 'min_duration', 139 | }, 140 | types: [ 141 | 'pornstar', 142 | 'video', 143 | ], 144 | }; 145 | -------------------------------------------------------------------------------- /src/utils/urlBuilder.js: -------------------------------------------------------------------------------- 1 | const QueryString = require('query-string'); 2 | const kebabCase = require('lodash.kebabcase'); 3 | const camelCase = require('lodash.camelcase'); 4 | const urls = require('../configs/urls'); 5 | const search = require('../configs/search'); 6 | 7 | /** 8 | * 9 | * @param options 10 | * @returns {string} 11 | * @private 12 | */ 13 | const getBaseVideoUrl = (options = {}) => { 14 | if (Array.isArray(options.categories) && options.categories.length > 0) { 15 | if (options.categories.length === 1) { 16 | const option = camelCase(options.categories[0]); 17 | return `${urls.baseUrl}${search.categories[option]}`; 18 | } 19 | const incategories = [ 20 | kebabCase(options.categories[0]).toLowerCase(), 21 | kebabCase(options.categories[1]).toLowerCase(), 22 | ].join('/'); 23 | 24 | return `${urls.incategories}/${incategories}`; 25 | } 26 | 27 | return urls.videoUrl; 28 | }; 29 | 30 | /** 31 | * 32 | * @param type 33 | * @returns {string} 34 | */ 35 | const getBaseSearchUrl = (type) => (type === 'video' ? urls.videoSearchUrl : urls.modelSearchUrl); 36 | 37 | /** 38 | * 39 | * @param params 40 | * @returns {{}} 41 | */ 42 | const getVideosParams = (params = {}) => Object.keys(params) 43 | .reduce((acc, key) => { 44 | const match = search.video[key]; 45 | if (match) acc[search.video[key]] = params[key]; 46 | 47 | return acc; 48 | }, {}); 49 | 50 | /** 51 | * 52 | * @param page 53 | * @returns {{page}} 54 | */ 55 | const getPageParams = (page) => (page ? { page } : {}); 56 | 57 | /** 58 | * 59 | * @param quality 60 | * @returns {*} 61 | */ 62 | const getQualityParams = (quality) => ( 63 | quality 64 | ? { [search.keys.quality]: search.quality[quality] } 65 | : {} 66 | ); 67 | 68 | /** 69 | * 70 | * @param production 71 | * @returns {*} 72 | */ 73 | const getProductionParams = (production) => ( 74 | production 75 | ? { [search.keys.production]: production } 76 | : {} 77 | ); 78 | 79 | /** 80 | * 81 | * @param ranking 82 | * @param range 83 | * @returns {{}} 84 | */ 85 | const getRankRangeParams = (ranking, range) => { 86 | const params = {}; 87 | if (search.rankings[ranking]) { 88 | params[search.keys.rankings] = search.rankings[ranking]; 89 | if (search.rangedRankings.includes(ranking) && search.ranges[range]) { 90 | params[search.keys.ranges] = search.ranges[range]; 91 | } 92 | } 93 | 94 | return params; 95 | }; 96 | 97 | /** 98 | * 99 | * @param duration 100 | * @returns {{}} 101 | */ 102 | const getDurationParams = (duration = {}) => Object.keys(search.duration) 103 | .reduce((acc, key) => { 104 | if (duration[key]) acc[search.duration[key]] = duration[key]; 105 | return acc; 106 | }, {}); 107 | 108 | /** 109 | * 110 | * @param options 111 | * @returns {`${string}?${string}`} 112 | * @private 113 | */ 114 | const buildVideosUrl = (options = {}) => { 115 | const defaultOptions = { 116 | ranking: '', 117 | range: '', 118 | ...options, 119 | }; 120 | const videoBase = getBaseVideoUrl(defaultOptions); 121 | let queryObject = QueryString.parseUrl(videoBase); 122 | 123 | queryObject = { 124 | ...queryObject.query, 125 | ...getVideosParams(defaultOptions.video), 126 | ...getPageParams(defaultOptions.page), 127 | ...getQualityParams(defaultOptions.quality), 128 | ...getDurationParams(defaultOptions.duration), 129 | ...getProductionParams(defaultOptions.production), 130 | ...getRankRangeParams(defaultOptions.ranking, defaultOptions.range), 131 | }; 132 | const queryString = QueryString.stringify(queryObject); 133 | const urlString = videoBase.split('?')[0]; // Remove latent queries. 134 | 135 | return `${urlString}${queryString.length ? '?' : ''}${queryString}`; 136 | }; 137 | 138 | /** 139 | * 140 | * @param query 141 | * @param type 142 | * @param options 143 | * @returns {string} 144 | */ 145 | const buildSearchUrl = (query, type, options) => { 146 | const searchBase = getBaseSearchUrl(type); 147 | 148 | const queryObject = { 149 | ...getPageParams(options.page), 150 | search: query, 151 | }; 152 | const queryString = QueryString.stringify(queryObject); 153 | 154 | return `${searchBase}${queryString.length ? '?' : ''}${queryString}`; 155 | }; 156 | 157 | /** 158 | * 159 | * @param url 160 | * @param options 161 | * @returns {string} 162 | */ 163 | const buildModelVideosUrl = (url, options) => { 164 | const queryObject = { 165 | ...getPageParams(options.page), 166 | }; 167 | const queryString = QueryString.stringify(queryObject); 168 | 169 | return `${url}${queryString.length ? '?' : ''}${queryString}`; 170 | }; 171 | 172 | module.exports = { 173 | buildVideosUrl, 174 | buildSearchUrl, 175 | buildModelVideosUrl, 176 | }; 177 | -------------------------------------------------------------------------------- /tests/pages/videos.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const assert = require('assert'); 3 | const nock = require('nock'); 4 | const ph = require('../../src'); 5 | const fixtures = require('../helpers/fixtures'); 6 | const matching = require('../helpers/matching'); 7 | 8 | describe('Videos Page', () => { 9 | before(() => nock.disableNetConnect()); 10 | after(() => nock.enableNetConnect()); 11 | 12 | it('should use the provided cookie in the request', async () => { 13 | const cookie = 'tinycookie;'; 14 | nock('https://www.pornhubpremium.com') 15 | .get('/video') 16 | .matchHeader('cookie', cookie) 17 | .reply(200, ''); 18 | 19 | await ph.authenticate({ cookie }); 20 | await ph.videos(); 21 | }); 22 | 23 | it('should correctly parameterize the page number', async () => { 24 | const expected = { page: '2' }; 25 | nock('https://www.pornhubpremium.com') 26 | .get('/video') 27 | .query(expected) 28 | .reply(200, ''); 29 | 30 | await ph.videos({ page: 2 }); 31 | }); 32 | 33 | it('should return a list of videos with no params', async () => { 34 | nock('https://www.pornhubpremium.com') 35 | .filteringPath(() => '/video') 36 | .get('/video') 37 | .reply(200, fixtures.getFixture('validVideosResult.html')); 38 | 39 | await ph.videos(); 40 | }); 41 | 42 | it('should return a single category as a param', async () => { 43 | const expected = { c: '42' }; 44 | nock('https://www.pornhubpremium.com') 45 | .get('/video') 46 | .query(expected) 47 | .reply(200, fixtures.getFixture('validVideosResult.html')); 48 | 49 | await ph.videos({ categories: ['Red Head'] }); 50 | }); 51 | 52 | it('should correctly parameterize the options', async () => { 53 | const expected = { 54 | download: '1', 55 | hd: '4', 56 | premium: '1', 57 | min_duration: '20', 58 | max_duration: '30', 59 | p: 'homemade', 60 | t: 't', 61 | o: 'mv', 62 | }; 63 | nock('https://www.pornhubpremium.com') 64 | .get('/video') 65 | .query(expected) 66 | .reply(200, fixtures.getFixture('validVideosResult.html')); 67 | 68 | await ph.videos({ 69 | video: { 70 | premiumOnly: 1, 71 | downloadOnly: 1, 72 | }, 73 | duration: { 74 | min: 20, 75 | max: 30, 76 | }, 77 | quality: '4k', 78 | production: 'homemade', 79 | ranking: 'mostViewed', 80 | range: 'daily', 81 | }); 82 | }); 83 | 84 | it('should should not set the range for an invalid ranking', async () => { 85 | const expected = { 86 | o: 'lg', 87 | }; 88 | nock('https://www.pornhubpremium.com') 89 | .get('/video') 90 | .query(expected) 91 | .reply(200, fixtures.getFixture('validVideosResult.html')); 92 | 93 | await ph.videos({ 94 | ranking: 'longest', 95 | range: 'daily', 96 | }); 97 | }); 98 | 99 | it('should return multiple categories as a new URL', async () => { 100 | nock('https://www.pornhubpremium.com') 101 | .get('/video/incategories/red-head/asian') 102 | .reply(200, fixtures.getFixture('validVideosResult.html')); 103 | 104 | await ph.videos({ 105 | categories: ['Red Head', 'Asian'], 106 | }); 107 | }); 108 | 109 | it('should return a single category as a query param', async () => { 110 | const expected = { c: '42' }; 111 | nock('https://www.pornhubpremium.com') 112 | .get('/video') 113 | .query(expected) 114 | .reply(200, fixtures.getFixture('validVideosResult.html')); 115 | 116 | await ph.videos({ 117 | categories: ['Red Head'], 118 | }); 119 | }); 120 | 121 | it('should return all videos on the pages', async () => { 122 | const expected = { 123 | title: 'Eliza Ibarra Wants To Get Creampied Over And Over To Celebrate The 4th Of July!', 124 | url: 'https://www.pornhubpremium.com/view_video.php?viewkey=ph60dca43f969c6', 125 | duration: '26:34', 126 | username: 'Cum4K', 127 | }; 128 | nock('https://www.pornhubpremium.com') 129 | .filteringPath(() => '/video/incategories/red-head/asian') 130 | .get('/video/incategories/red-head/asian') 131 | .reply(200, fixtures.getFixture('validVideosResult.html')); 132 | 133 | const videoPage = await ph.videos(); 134 | const videos = videoPage.getVideos(); 135 | 136 | const hasExpected = matching.findMatchingKeys(expected, videos); 137 | assert.ok(hasExpected); 138 | }); 139 | 140 | it('should return a page number', async () => { 141 | nock('https://www.pornhubpremium.com') 142 | .get('/video') 143 | .query({ page: '2' }) 144 | .reply(200, ''); 145 | 146 | const videos = await ph.videos({ page: 2 }); 147 | const pageNumber = videos.getPage(); 148 | 149 | assert.equal(pageNumber, 2); 150 | }); 151 | 152 | it('should default the page number to 1', async () => { 153 | nock('https://www.pornhubpremium.com') 154 | .get('/video') 155 | .reply(200, ''); 156 | 157 | const videos = await ph.videos(); 158 | const pageNumber = videos.getPage(); 159 | 160 | assert.equal(pageNumber, 1); 161 | }); 162 | 163 | it('should return whether the query returned matches', async () => { 164 | nock('https://www.pornhubpremium.com') 165 | .get('/video') 166 | .reply(200, fixtures.getFixture('validVideosResult.html')); 167 | 168 | const videos = await ph.videos(); 169 | const hasResults = videos.hasResults(); 170 | 171 | assert.equal(hasResults, false); 172 | }); 173 | 174 | it('should return an array of sidebar categories', async () => { 175 | const expected = { name: 'Popular With Women', count: 142, url: 'https://www.pornhubpremium.com/popularwithwomen' }; 176 | nock('https://www.pornhubpremium.com') 177 | .filteringPath(() => '/video/incategories/red-head/asian') 178 | .get('/video/incategories/red-head/asian') 179 | .reply(200, fixtures.getFixture('validVideosResult.html')); 180 | 181 | const videoPage = await ph.videos(); 182 | const categories = videoPage.getCategories(); 183 | 184 | const hasExpected = matching.findMatchingKeys(expected, categories); 185 | assert.ok(hasExpected); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pornhub Premium API (Alpha) 2 | [![NotASmurf](https://circleci.com/gh/notasmurf/pornhubpremium-api.svg?style=svg)](https://github.com/notasmurf/pornhubpremium-api) [![Coverage Status](https://coveralls.io/repos/github/notasmurf/pornhubpremium-api/badge.svg?branch=main)](https://coveralls.io/github/notasmurf/pornhubpremium-api?branch=main) 3 | 4 | Listen here you mankey lovin' _gits_ as this poop nugget of an Application Phappable Interface is the best you are going to get for scraping PornHub Premium (at least in a worthwhile language). So pay attention. 5 | 6 | **Table of Contents** 7 | 8 | 9 | 10 | - [Usage](#usage) 11 | - [Authentication](#authentication) - Logging in. 12 | - [Videos](#videos) - Searching the videos page. 13 | - [Video](#video) - Visiting a particular video. 14 | - [Search](#search) - Performing video/model search. 15 | - [Model](#model) - For searching specific models. 16 | - [Model Videos](#model-videos) - View collection of model's videos. 17 | - [Coming Soon](#coming-soon) 18 | - [FAQs](#faqs) 19 | 20 | 21 | ## Installation 22 | ```bash 23 | npm i @notasmurf/pornhubpremium-api 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Authentication 29 | You'll first need to log in. For now, just physically log in and grab the cookie returned in the one of the subsequent page load calls. It's a long lasting token anyway. I am working to figure out regular auth. 30 | 31 | Example 32 | ```js 33 | const ph = require('@notasmurf/pornhubpremium-api'); 34 | 35 | await ph.authenticate({ 36 | cookie: 'some-cookie', 37 | }); 38 | ``` 39 | 40 | ### Videos 41 | I bet you want to get all the links on the main videos page so you can add them to your spank bank. I got your back, bea. But just your back. 42 | ```js 43 | const pageVideos = await ph.videos({ 44 | video: { 45 | premiumOnly: 1, 46 | downloadOnly: 1, 47 | }, 48 | duration: { 49 | min: 0, 50 | max: 30, 51 | }, 52 | raking: 'mostViewed', 53 | range: 'weekly', 54 | quality: '4k', 55 | categories: ['Red Head', 'Verified Amateurs'], 56 | page: 1, 57 | }); 58 | 59 | console.log(videos.hasResults()); // Returns true if query returned results. 60 | console.log(videos.getVideos()); // Returns ALL videos on page. 61 | /** Print 62 | [ 63 | { 64 | title: 'Mofos - Scott Nails Teaches His Step Daughter Angelica Cruz How To Bend Over For Ass Fucking', 65 | url: 'https://www.pornhubpremium.com/view_video.php?viewkey=ph60d5d5a7a55fb', 66 | duration: '34:14', 67 | username: 'Lets Try Anal' 68 | } 69 | ] 70 | */ 71 | 72 | // Handy function for returning all the spank bank categories on the sidebar menu. 73 | conole.log(videos.getCategories()); 74 | /** 75 | [ 76 | { 77 | url: 'https://www.pornhubpremium.com/categories/babe', 78 | name: 'Babe' 79 | } 80 | ] 81 | */ 82 | ``` 83 | 84 |
85 | Here are the supported arguments for the `.videos` function. (click to expand) 86 | 87 | | argument | type | description | default | options | 88 | | :--- | :--- | :--- | :--- | :--- | 89 | | `video` | object | enables premium or download only options | both false by default | `premiumOnly` and `downloadOnly`, passed as 0 or 1 | 90 | | `duration` | object | sets the specified min and max duration option | widest range by default | `min_duration` and `max_duration` as ints in units of 10 | 91 | | `quality` | string | sets the min resolution | widest range by default | `4k`, `1440p`, `1080p`, `all` 92 | | `category` | array | sets the categories for which to search | all categories | all categories on pornhub in camel case | 93 | | `production` | string | sets the production type of the search results | both types | `homemade`, `professional` | 94 | | `ranking` | string | sets the ranking of the search results | defaults to featured videos | `mostViewed`, `topRated`, `hottest`, `longest`, `newest` | 95 | | `range` | string | sets the upload time range for the video result (for `mostViewed` and `topRated` only) | defaults to `weekly` | `daily`, `weekly`, `monthly`, `yearly`, `allTime` | 96 | | `page` | number | sets the pagination number | defaults to first page | any whole numbers >= 1 | 97 |
98 | 99 | 100 | ### Video 101 | Now that you got your dirty list of naughtiables, let's get some download links going. Ol' `@notasmurf` has GOT you covered. 102 | ```js 103 | const video = await ph.video('https://www.pornhubpremium.com/view_video.php?viewkey=ph60b9e26695e28'); 104 | 105 | // Get all downloadable video links (if present). 106 | console.log(video.getDownloads()); 107 | /** Example 108 | [ 109 | { 110 | definition: 'HD 1080p', 111 | url: 'https://ed.phprcdn.com/videos/202106/25/390180751/1080P_4000K_390180751.mp4?validfrom=1625441105&validto=1625448305&rate=50000k&burst=50000k&ip=71.241.248.52&ipa=71.241.248.52&hash=Z5O1%2B%2FQdGD4cvb5Ll3PByRg04bc%3D' 112 | } 113 | ] 114 | */ 115 | 116 | // Print the models associated with the video. 117 | console.log(video.getModels()); 118 | /** Example 119 | [ 120 | { 121 | name: 'Bethany Benz', 122 | url: 'https://www.pornhubpremium.com/pornstar/bethany-benz' 123 | } 124 | ] 125 | */ 126 | ``` 127 | 128 | 129 | ### Search 130 | Okay, so now for the good stuff. You can do some plaintext searches as well, for both models and videos. See below for the details. 131 | 132 | #### For Models: 133 | ```js 134 | const models = await ph.search('Abby', 'pornstars', { page: 1 }); // Options not required. Defaults to 1. 135 | 136 | console.log(models.getModels()); 137 | 138 | /** Example 139 | [ 140 | { rank: '8041', name: 'Abby Lane', videos: '12 Videos' }, 141 | { rank: '9762', name: 'Abby Lexus', videos: '4 Videos' }, 142 | { rank: '15018', name: "Abby O'Toole", videos: '3 Videos' }, 143 | { rank: '14856', name: 'Abbie Jordyn', videos: '3 Videos' }, 144 | { rank: '1697', name: 'Abby Cross', videos: '130 Videos' } 145 | ] 146 | */ 147 | ``` 148 | 149 | #### For Videos: 150 | ```js 151 | const models = await ph.search('Redhead', 'video', { page: 1 }); // Options not required. Defaults to 1. 152 | 153 | console.log(models.getModels()); 154 | 155 | /** Example 156 | [ 157 | { 158 | title: 'HOT REDHEAD STEPDAUGHTER LACY LENNON SQUIRTS OUT MY ACCIDENTAL CREAMPIE', 159 | url: 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5c0c89e719f50', 160 | duration: '44:26', 161 | username: 'Spank Monster' 162 | } 163 | ] 164 | */ 165 | ``` 166 | 167 | 168 | ### Model 169 | So now that you've just used my tool for the sole purpose of scraping PornHub for asian pornstars with big tits, let's use this tool to scrape your oriental honey's personal page. 170 | 171 | ```js 172 | const model = await ph.model('https://www.pornhubpremium.com/pornstar/britney-amber'); 173 | const details = model.getDetailedInfo(); 174 | 175 | console.log(details); 176 | /** Example: 177 | { 178 | relationshipStatus: 'Single', 179 | interestedIn: 'Guys and Girls', 180 | cityAndCountry: 'Southern California, US', 181 | pornstarProfileViews: '35,568,683', 182 | careerStatus: 'Active', 183 | careerStartAndEnd: '2008 to Present', 184 | gender: 'Female', 185 | birthPlace: 'Banning, California, United States of America', 186 | starSign: 'Scorpio', 187 | measurements: '36D-24-34', 188 | height: '5 ft 5 in (165 cm)', 189 | weight: '115 lbs (52 kg)', 190 | ethnicity: 'White', 191 | hairColor: 'Blonde', 192 | fakeBoobs: 'Yes', 193 | tattoos: 'Yes', 194 | piercings: 'Yes', 195 | interestsAndHobbies: 'Archery and Bowhunting', 196 | hometown: 'Southern California', 197 | profileViews: '21,998,089', 198 | videosWatched: '51', 199 | } 200 | */ 201 | 202 | const videos = model.getVideos(); // ONLY shows videos on the immediate page. 203 | /** Example 204 | [ 205 | { 206 | title: 'Big Booty Teen Slut Abby Gets Fucked By Her Perverted Stepdad', 207 | url: 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5dc77449561a1', 208 | duration: '24:35', 209 | username: 'Exposed Whores' 210 | } 211 | ] 212 | */ 213 | ``` 214 | 215 | ### Model Videos 216 | Not enough videos for you on your model's personal page? Christ, man. You're a fucking addict. But I got you covered, baby bird. Here's all you need. 217 | 218 | ```js 219 | const model = await ph.model('https://www.pornhubpremium.com/pornstar/britney-amber'); 220 | 221 | // Use the following to get a complete list of 222 | const moreVideosUrl = model.getMoreVideosUrl(); 223 | console.log(moreVideosUrl); 224 | // Example: https://www.pornhubpremium.com/pornstar/britney-amber/videos 225 | 226 | const morePremiumVideosUrl = model.getMorePremiumVideosUrl(); 227 | console.log(morePremiumVideosUrl); 228 | // Example: https://www.pornhubpremium.com/pornstar/britney-amber/videos?premium=1 229 | 230 | const modelVideos = await ph.modelVieos(morePremiumVideosUrl, { page: 1 }); // Options not required. Defaults to 1. 231 | console.log(modelVideos); 232 | /** Example: 233 | [ 234 | { 235 | title: 'MileHigh - Two Horny Lesbians Britney Amber And Ivy Lebelle Love Anal Play', 236 | url: 'https://www.pornhubpremium.com/view_video.php?viewkey=ph5eaac125892e4', 237 | duration: '29:27', 238 | username: 'Sweetheart Video' 239 | } 240 | ] 241 | */ 242 | ``` 243 | 244 | ## COMING SOON 245 | * More search features 246 | * Favorites page 247 | * A better readme 248 | * If you want more, make a request on the Issues page. 249 | 250 | ## FAQs: 251 | **1) When will this package be ready?** 252 | 253 | I'll move it out of alpha once I have finished enough of the core features. I don't expect to make any major breaking changes though, although there will of course be bugs, so go ahead and give it a whirl. 254 | 255 | **2) Will this thing be production ready after alpha?** 256 | 257 | Why...why the hell would you need that... 258 | 259 | **3) How do I contribute?** 260 | 261 | Make a PR. Why is this always a FAQ question? Git has one purpose. 262 | 263 | **4) This is PornHub. Can we hire you?** 264 | 265 | Tell you what. If you have the balls to make a meaningful PR to this repo, I will abso-fucking-lutely apply. 266 | -------------------------------------------------------------------------------- /tests/fixtures/loginPage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Pornhub Premium - We Have the Best Porn Videos 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 46 | 47 | 48 | 49 | 70 | 71 | 72 | 73 | 74 | 89 | 90 | 91 | 92 | 93 | 94 |
95 |
96 |
97 | 102 |
103 | 112 |
113 |
114 |
115 | 116 |
117 |
118 |

Welcome to the new Pornhub Premium

119 |

Content on Pornhub Premium can only be accessed by our members.

120 |
121 |
122 | Pornhub Premium - No ads 123 |

No ads

124 |
125 |
126 | Pornhub Premium - Exclusive content 127 |

Exclusive content

128 |
129 |
130 | Pornhub Premium - High quality HD 131 |

High quality HD

132 |
133 |
134 | Pornhub Premium - Cancel anytime 135 |

Cancel anytime

136 |
137 |

Exclusive full-length HD videos from:

138 | Pornhub Premium - Content partners logo 139 |
140 | 141 | 145 | 146 |

Already a Pornhub Premium Member ? Log In.

147 |
148 |
149 | 150 |
151 |
152 |
153 |
154 |

This website contains information, links, images and videos of the Pornhub Premium explicit material. If you are under the age of 18, if such material offends you or if it's illegal to view such material in your community please do not continue.

155 |

Please read and comply with the following conditions before you continue:

156 |

157 | This website contains age-restricted materials. 158 | If you are under the age of 18 years, or under the age of majority in the location from where you are accessing this website you do not have authorization or permission to enter this website or access any of its materials. 159 | If you are over the age of 18 years or over the age of majority in the location from where you are accessing this website by entering the website you hereby agree to comply with all the TERMS AND CONDITIONS. 160 | You also acknowledge and agree that you are not offended by nudity and explicit depictions of sexual activity.

161 |

By clicking on the "Enter" button, and by entering this website you agree with all the above and certify under penalty of perjury that you are an adult.

162 |
163 | 167 |
168 | 178 |

179 | The ICRA content tagging system has been willingly implemented on PornhubPremium and all its affiliated websites in order to establish a higher level of parental control throughout our entire network. 180 | In accordance with Microsoft Internet explorer's content filtering function and ICRA.org, our network has been unconditionally rated for content in able to maintain the proper standard of required compatibility. 181 |
182 | Pornhub is rated with RTA label. Parents, you can easily block access to this site. Please read this page http://www.rtalabel.org/index.php?content=parents/ for more information.

183 |
184 |
185 |
186 |
187 | 188 | 231 |
232 | 233 | 251 | 252 | 253 | 254 | 255 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 284 | --------------------------------------------------------------------------------