├── .github ├── FUNDING.yml └── workflows │ └── sync-tests.yml ├── LICENSE ├── README.md ├── README └── Changelog.md ├── setup.py └── xnxx_api ├── __init__.py ├── modules ├── __init__.py ├── category.py ├── consts.py ├── errors.py └── search_filters.py ├── tests ├── __init__.py ├── test_search.py ├── test_user.py └── test_video.py └── xnxx_api.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: "https://paypal.me/EchterAlsFake" -------------------------------------------------------------------------------- /.github/workflows/sync-tests.yml: -------------------------------------------------------------------------------- 1 | name: Sync API test 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | 7 | on: 8 | push: 9 | pull_request: 10 | schedule: 11 | - cron: '0 0 * * 0' # Runs every Sunday at 00:00 UTC 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python 3.11 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: 3.11 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install pytest 28 | pip install . 29 | - name: Test with pytest 30 | run: | 31 | pytest 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright © 2007 Free Software Foundation, Inc. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 7 | 8 | This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 9 | 10 | 0. Additional Definitions. 11 | As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. 12 | 13 | “The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. 14 | 15 | An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. 16 | 17 | A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. 18 | 19 | The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. 20 | 21 | The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 22 | 23 | 1. Exception to Section 3 of the GNU GPL. 24 | You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 25 | 26 | 2. Conveying Modified Versions. 27 | If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: 28 | 29 | a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or 30 | b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 31 | 3. Object Code Incorporating Material from Library Header Files. 32 | The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: 33 | 34 | a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. 35 | b) Accompany the object code with a copy of the GNU GPL and this license document. 36 | 4. Combined Works. 37 | You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: 38 | 39 | a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. 40 | b) Accompany the Combined Work with a copy of the GNU GPL and this license document. 41 | c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. 42 | d) Do one of the following: 43 | 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 44 | 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. 45 | e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 46 | 5. Combined Libraries. 47 | You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: 48 | 49 | a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. 50 | b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 51 | 6. Revised Versions of the GNU Lesser General Public License. 52 | The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 53 | 54 | Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. 55 | 56 | If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

XNXX API

2 | 3 |
4 | Downloads 5 | Downloads (Async) 6 | CodeQL Analysis 7 | API Tests 8 | API Tests 9 |
10 | 11 | # Description 12 | 13 | XNXX API is an API for xnxx.com. It allows you to fetch information from videos using regexes and requests. 14 | 15 | # Disclaimer 16 | 17 | > [!IMPORTANT] 18 | > XNXX API is in violation to the ToS of xnxx.com! 19 | > If you are the website owner of xnxx.com, contact me at my E-Mail, and I'll take this repository immediately offline. 20 | > EchterAlsFake@proton.me 21 | 22 | # Quickstart 23 | 24 | ### Have a look at the [Documentation](https://github.com/EchterAlsFake/API_Docs/blob/master/Porn_APIs/XNXX.md) for more details 25 | 26 | - Install the library with `pip install xnxx_api` 27 | 28 | 29 | ```python 30 | from xnxx_api import Client 31 | # Initialize a Client object 32 | client = Client() 33 | 34 | # Fetch a video 35 | video_object = client.get_video("") 36 | 37 | # Information from Video objects 38 | print(video_object.title) 39 | print(video_object.likes) 40 | # Download the video 41 | 42 | video_object.download(downloader="threaded", quality="best", path="your_output_path + filename") 43 | 44 | # SEE DOCUMENTATION FOR MORE 45 | ``` 46 | 47 | > [!NOTE] 48 | > XNXX API can also be used from the command line. Do: xnxx_api -h to see the options 49 | 50 | # Changelog 51 | See [Changelog](https://github.com/EchterAlsFake/xnxx_api/blob/master/README/Changelog.md) for more details. 52 | 53 | # Support (Donations) 54 | I am developing all my projects entirely for free. I do that because I have fun and I don't want 55 | to charge 30€ like other people do. 56 | 57 | However, if you find my work useful, please consider donating something. A tiny amount such as 1€ 58 | means a lot to me. 59 | 60 | Paypal: https://paypal.me/EchterAlsFake 61 |
XMR (Monero): `42XwGZYbSxpMvhn9eeP4DwMwZV91tQgAm3UQr6Zwb2wzBf5HcuZCHrsVxa4aV2jhP4gLHsWWELxSoNjfnkt4rMfDDwXy9jR` 62 | 63 | 64 | # Contribution 65 | Do you see any issues or having some feature requests? Simply open an Issue or talk 66 | in the discussions. 67 | 68 | Pull requests are also welcome. 69 | 70 | # License 71 | Licensed under the LGPLv3 License 72 |
Copyright (C) 2023–2025 Johannes Habel 73 | -------------------------------------------------------------------------------- /README/Changelog.md: -------------------------------------------------------------------------------- 1 | # 1.0 2 | - Initial release 3 | 4 | # 1.1 5 | - You can now pass `quality` and `threading` argument as a string, instead of object. 6 | - : See Documentation - `Locals` 7 | 8 | # 1.2 9 | - Fixed `likes`, `dislikes`, `comment_count` 10 | - Added Tests to CI / CD actions 11 | 12 | # 1.3 13 | - code refactoring 14 | - added model support 15 | - added searching support 16 | - updated documentation 17 | 18 | # 1.4 19 | - fixed an issue with video loading 20 | 21 | # 1.4.1 22 | - added support for mode searching #2 23 | 24 | # 1.5.0 25 | - proxy support (See Documentation) 26 | - updated to eaf_base_api v2 27 | - written tests for search and user objects 28 | - removed headers, cuz they were broken 29 | - type hinting -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="xnxx_api", 5 | version="1.5.3", 6 | packages=find_packages(), 7 | install_requires=["bs4", "eaf_base_api"], 8 | entry_points={ 9 | 'console_scripts': ['xnxx_api=xnxx_api.xnxx_api:main' 10 | # If you want to create any executable scripts 11 | ], 12 | }, 13 | author="Johannes Habel", 14 | author_email="EchterAlsFake@proton.me", 15 | description="A Python API for the Porn Site xnxx.com", 16 | long_description=open('README.md').read(), 17 | long_description_content_type='text/markdown', 18 | license="LGPLv3", 19 | url="https://github.com/EchterAlsFake/xnxx_api", 20 | classifiers=[ 21 | # Classifiers help users find your project on PyPI 22 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 23 | "Programming Language :: Python", 24 | ], 25 | ) -------------------------------------------------------------------------------- /xnxx_api/__init__.py: -------------------------------------------------------------------------------- 1 | # xnxx_api/__init__.py 2 | 3 | __all__ = [ 4 | "Client", "BaseCore", "Video", "Callback", 5 | "errors", "consts", "search_filters", "category" 6 | ] 7 | 8 | # Public API from xnxx_api.py 9 | from xnxx_api.xnxx_api import Client, BaseCore, Video, Callback 10 | from xnxx_api.modules import errors, consts, category, search_filters -------------------------------------------------------------------------------- /xnxx_api/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EchterAlsFake/xnxx_api/75579eccf5c3edcaf9dae458e3d75009ee6a6988/xnxx_api/modules/__init__.py -------------------------------------------------------------------------------- /xnxx_api/modules/category.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EchterAlsFake/xnxx_api/75579eccf5c3edcaf9dae458e3d75009ee6a6988/xnxx_api/modules/category.py -------------------------------------------------------------------------------- /xnxx_api/modules/consts.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # ROOT URLs 4 | ROOT_URL = "https://www.xnxx.com/" 5 | 6 | # REGEX 7 | REGEX_VIDEO_CHECK = re.compile(r"xnxx.com/(.*?)") 8 | REGEX_VIDEO_TITLE = re.compile(r"html5player\.setVideoTitle\('([^']*)'\);") 9 | REGEX_VIDEO_UPLOADER = re.compile(r"html5player\.setUploaderName\('([^']*)'\);") 10 | REGEX_VIDEO_LIKES = re.compile(r'(.*?)', re.DOTALL) 11 | REGEX_VIDEO_DISLIKES = re.compile(r'(.*?)', re.DOTALL) 12 | REGEX_VIDEO_COMMENT_COUNT = re.compile(r'(.*?)') 13 | REGEX_VIDEO_PORNSTARS = re.compile(r'') 14 | REGEX_VIDEO_KEYWORDS = re.compile(r'') 15 | REGEX_VIDEO_M3U8 = re.compile(r"html5player\.setVideoHLS\('([^']+)'\);") 16 | 17 | REGEX_SCRAPE_VIDEOS = re.compile(r'
(.*?)') 20 | REGEX_MODEL_TOTAL_PAGES = re.compile(r'') 21 | REGEX_MODEL_TOTAL_VIDEO_VIEWS = re.compile(r' (.*?) video views') -------------------------------------------------------------------------------- /xnxx_api/modules/errors.py: -------------------------------------------------------------------------------- 1 | class InvalidUrl(Exception): 2 | def __init__(self, msg): 3 | self.msg = msg 4 | 5 | 6 | class InvalidResponse(Exception): 7 | def __init__(self, msg): 8 | self.msg = msg 9 | -------------------------------------------------------------------------------- /xnxx_api/modules/search_filters.py: -------------------------------------------------------------------------------- 1 | class Length: 2 | X_0_10min = "/0-10min" 3 | X_10min_plus = "/10min+" 4 | X_10_20min = "/10-20min" 5 | X_20min_plus = "/20min+" 6 | 7 | 8 | class UploadTime: 9 | year = "/year" 10 | month = "/month" 11 | 12 | 13 | class SearchingQuality: 14 | X_720p = "/hd-only" 15 | X_1080p_plus = "/fullhd" 16 | 17 | class Mode: 18 | default = "" 19 | hits = "/hits" 20 | random = "/random" 21 | -------------------------------------------------------------------------------- /xnxx_api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EchterAlsFake/xnxx_api/75579eccf5c3edcaf9dae458e3d75009ee6a6988/xnxx_api/tests/__init__.py -------------------------------------------------------------------------------- /xnxx_api/tests/test_search.py: -------------------------------------------------------------------------------- 1 | from ..xnxx_api import Client 2 | 3 | client = Client() 4 | search = client.search("fortnite") 5 | 6 | def test_search(): 7 | for idx, video in enumerate(search.videos): 8 | assert isinstance(video.title, str) 9 | 10 | if idx == 3: 11 | break -------------------------------------------------------------------------------- /xnxx_api/tests/test_user.py: -------------------------------------------------------------------------------- 1 | from ..xnxx_api import Client 2 | 3 | client = Client() 4 | user = client.get_user("https://www.xnxx.com/pornstar/cory-chase") 5 | objects_video = ["title", "publish_date", "length", "author"] 6 | 7 | 8 | 9 | def test_video_views(): 10 | assert isinstance(user.total_video_views, str) 11 | assert user.total_videos > 0 12 | 13 | def test_videos(): 14 | for idx, video in enumerate(user.videos): 15 | if idx == 3: 16 | break 17 | for object in objects_video: 18 | assert isinstance(getattr(video, object), str) 19 | 20 | -------------------------------------------------------------------------------- /xnxx_api/tests/test_video.py: -------------------------------------------------------------------------------- 1 | from ..xnxx_api import Video 2 | url = "https://www.xnxx.com/video-1b9bufc9/die_zierliche_stieftochter_passt_kaum_in_den_mund_ihres_stiefvaters" 3 | # This will be the URL for all tests 4 | 5 | video = Video(url) 6 | 7 | 8 | def test_video_title(): 9 | title = video.title 10 | assert isinstance(title, str) and len(title) > 0 11 | 12 | 13 | def test_author(): 14 | author = video.author 15 | assert isinstance(author, str) and len(author) > 0 16 | 17 | 18 | def test_length(): 19 | length = video.length 20 | assert isinstance(length, str) and len(length) > 0 21 | 22 | 23 | def test_highest_quality(): 24 | highest_quality = video.highest_quality 25 | assert isinstance(highest_quality, str) and len(highest_quality) > 0 26 | 27 | 28 | def test_views(): 29 | views = video.views 30 | assert isinstance(views, str) and len(views) > 0 31 | 32 | 33 | def test_comment_count(): 34 | comment_count = video.comment_count 35 | assert isinstance(comment_count, str) 36 | 37 | 38 | def test_likes(): 39 | likes = video.likes 40 | assert isinstance(likes, str) 41 | 42 | 43 | def test_dislikes(): 44 | dislikes = video.dislikes 45 | assert isinstance(dislikes, str) 46 | 47 | 48 | def test_pornstars(): 49 | pornstars = video.pornstars 50 | assert isinstance(pornstars, list) # Some videos don't contain pornstars, which is why we don't test it here 51 | 52 | 53 | def test_description(): 54 | description = video.description 55 | assert isinstance(description, str) and len(description) > 0 56 | 57 | 58 | def test_keywords(): 59 | tags = video.tags 60 | assert isinstance(tags, list) and len(tags) > 0 61 | 62 | 63 | def test_thumbnail_url(): 64 | thumbnail_url = video.thumbnail_url 65 | assert isinstance(thumbnail_url, list) and len(thumbnail_url) > 0 66 | 67 | 68 | def test_publish_date(): 69 | publish_date = video.publish_date 70 | assert isinstance(publish_date, str) and len(publish_date) > 0 71 | 72 | 73 | def test_content_url(): 74 | content_url = video.content_url 75 | assert isinstance(content_url, str) and len(content_url) > 0 76 | 77 | 78 | def test_get_segments(): 79 | segments = list(video.get_segments(quality="best")) 80 | assert len(segments) > 10 81 | 82 | def test_download_low(): 83 | assert video.download(quality="worst", downloader="threaded") is True -------------------------------------------------------------------------------- /xnxx_api/xnxx_api.py: -------------------------------------------------------------------------------- 1 | try: 2 | from modules.consts import * 3 | from modules.errors import * 4 | from modules.search_filters import * 5 | 6 | except (ModuleNotFoundError, ImportError): 7 | from .modules.consts import * 8 | from .modules.errors import * 9 | from .modules.search_filters import * 10 | 11 | import os 12 | import html 13 | import json 14 | import logging 15 | import argparse 16 | import traceback 17 | 18 | from typing import Union, Generator 19 | from bs4 import BeautifulSoup 20 | from functools import cached_property 21 | from base_api import BaseCore, Callback 22 | from base_api.base import setup_logger 23 | 24 | 25 | core = BaseCore() 26 | 27 | 28 | def refresh_core(enable_logging=False, log_file: str = None, level = None): # Needed for Porn Fetch 29 | global core 30 | core = BaseCore() 31 | if enable_logging: 32 | core.enable_logging(log_file=log_file, level=level) 33 | 34 | 35 | 36 | class Video: 37 | def __init__(self, url): 38 | self.url = url 39 | self.available_m3u8_urls = None 40 | self.script_content = None 41 | self.html_content = None 42 | self.metadata_matches = None 43 | self.json_content = None 44 | self.logger = setup_logger(name="XNXX API - [Video]", log_file=None, level=logging.CRITICAL) 45 | 46 | if not REGEX_VIDEO_CHECK.search(self.url): 47 | raise InvalidUrl("The video URL is invalid!") 48 | 49 | else: 50 | self.get_base_html() 51 | self.get_script_content() 52 | self.get_metadata_matches() 53 | self.extract_json_from_html() 54 | 55 | def enable_logging(self, log_file, level): 56 | self.logger = setup_logger(name="XNXX API - [Video]", log_file=log_file, level=level) 57 | 58 | 59 | def get_base_html(self) -> None: 60 | self.html_content = core.fetch(url=self.url) 61 | 62 | @classmethod 63 | def is_desired_script(cls, tag): 64 | if tag.name != "script": 65 | return False 66 | script_contents = ['html5player', 'setVideoTitle', 'setVideoUrlLow'] 67 | return all(content in tag.text for content in script_contents) 68 | 69 | def get_metadata_matches(self) -> None: 70 | soup = BeautifulSoup(self.html_content, 'html.parser') 71 | metadata_span = soup.find('span', class_='metadata') 72 | metadata_text = metadata_span.get_text() 73 | 74 | # Use a regex to extract the desired strings 75 | self.metadata_matches = re.findall(r'(\d+min|\d+p|\d[\d.,]*\s*[views]*)', metadata_text) 76 | 77 | def get_script_content(self) -> None: 78 | soup = BeautifulSoup(self.html_content, "html.parser") 79 | target_script = soup.find(self.is_desired_script) 80 | if target_script: 81 | self.script_content = target_script.text 82 | 83 | else: 84 | raise InvalidResponse("Couldn't extract JSON from HTML") 85 | 86 | def extract_json_from_html(self) -> None: 87 | soup = BeautifulSoup(self.html_content, "html.parser") 88 | # Find the