xhamster_api 2.0__tar.gz → 2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xhamster_api-2.0 → xhamster_api-2.2}/PKG-INFO +54 -19
- {xhamster_api-2.0 → xhamster_api-2.2}/README.md +52 -15
- {xhamster_api-2.0 → xhamster_api-2.2}/pyproject.toml +3 -3
- xhamster_api-2.2/xhamster_api/modules/type_hints.py +4 -0
- {xhamster_api-2.0 → xhamster_api-2.2}/xhamster_api/xhamster_api.py +41 -21
- xhamster_api-2.0/xhamster_api/modules/type_hints.py +0 -3
- {xhamster_api-2.0 → xhamster_api-2.2}/LICENSE +0 -0
- {xhamster_api-2.0 → xhamster_api-2.2}/xhamster_api/__init__.py +0 -0
- {xhamster_api-2.0 → xhamster_api-2.2}/xhamster_api/modules/__init__.py +0 -0
- {xhamster_api-2.0 → xhamster_api-2.2}/xhamster_api/modules/consts.py +0 -0
- {xhamster_api-2.0 → xhamster_api-2.2}/xhamster_api/modules/errors.py +0 -0
- {xhamster_api-2.0 → xhamster_api-2.2}/xhamster_api/tests/__init__.py +0 -0
- {xhamster_api-2.0 → xhamster_api-2.2}/xhamster_api/tests/test_all.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xhamster_api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
4
4
|
Summary: A Python API for the Porn Site xhamster.com
|
|
5
5
|
Author: Johannes Habel
|
|
6
6
|
Author-email: Johannes Habel <EchterAlsFake@proton.me>
|
|
@@ -14,9 +14,7 @@ Requires-Dist: eaf-base-api
|
|
|
14
14
|
Requires-Dist: m3u8
|
|
15
15
|
Requires-Dist: av ; python_full_version >= '3.10' and extra == 'av'
|
|
16
16
|
Requires-Dist: lxml ; extra == 'full'
|
|
17
|
-
Requires-
|
|
18
|
-
Requires-Dist: httpx[socks] ; extra == 'full'
|
|
19
|
-
Requires-Python: >=3.10
|
|
17
|
+
Requires-Python: >=3.12
|
|
20
18
|
Project-URL: Homepage, https://github.com/EchterAlsFake/xhamster_api
|
|
21
19
|
Provides-Extra: av
|
|
22
20
|
Provides-Extra: full
|
|
@@ -34,10 +32,45 @@ Description-Content-Type: text/markdown
|
|
|
34
32
|
XHamster API is an API for xhamster.com. It allows you to fetch information from videos using regexes and requests.
|
|
35
33
|
|
|
36
34
|
# Disclaimer
|
|
37
|
-
> [!IMPORTANT]
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
>
|
|
35
|
+
> [!IMPORTANT]
|
|
36
|
+
> This is an unofficial and unaffiliated project. Please read the full disclaimer before use:
|
|
37
|
+
> **[DISCLAIMER.md](https://github.com/EchterAlsFake/API_Docs/blob/master/Disclaimer.md)**
|
|
38
|
+
>
|
|
39
|
+
> By using this project you agree to comply with the target site’s rules, copyright/licensing requirements,
|
|
40
|
+
> and applicable laws. Do not use it to bypass access controls or scrape at disruptive rates.
|
|
41
|
+
|
|
42
|
+
# Features
|
|
43
|
+
- Fetch videos + metadata
|
|
44
|
+
- Download videos
|
|
45
|
+
- Fetch Channels
|
|
46
|
+
- Fetch Pornstars
|
|
47
|
+
- Fetch Creators
|
|
48
|
+
- Fetch Shorts
|
|
49
|
+
- Search for videos
|
|
50
|
+
- Fetch playlists
|
|
51
|
+
- Asynchronous
|
|
52
|
+
- Built-in caching
|
|
53
|
+
- Easy interface
|
|
54
|
+
- Great type hinting
|
|
55
|
+
|
|
56
|
+
#### Networking Features
|
|
57
|
+
- HTTP 2.0 / HTTP 3.0
|
|
58
|
+
- Browser impersonation
|
|
59
|
+
- Custom JA3
|
|
60
|
+
- All proxy types
|
|
61
|
+
- Proxy authentication
|
|
62
|
+
- Speed Limit
|
|
63
|
+
- DNS over HTTPS
|
|
64
|
+
- And even more...
|
|
65
|
+
- All of this is configurable and can be adjusted as you like!
|
|
66
|
+
|
|
67
|
+
# Supported Platforms
|
|
68
|
+
This API has been tested and confirmed working on:
|
|
69
|
+
|
|
70
|
+
- Windows 11 (x64)
|
|
71
|
+
- macOS Sequoia (x86_64)
|
|
72
|
+
- Linux (Arch) (x86_64)
|
|
73
|
+
- Android 16 (aarch64)
|
|
41
74
|
|
|
42
75
|
# Quickstart
|
|
43
76
|
|
|
@@ -48,17 +81,19 @@ XHamster API is an API for xhamster.com. It allows you to fetch information from
|
|
|
48
81
|
```python
|
|
49
82
|
from xhamster_api import Client
|
|
50
83
|
# Initialize a Client object
|
|
51
|
-
client = Client()
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
video_object.
|
|
85
|
+
async def main():
|
|
86
|
+
client = Client()
|
|
87
|
+
|
|
88
|
+
# Fetch a video
|
|
89
|
+
video_object = await client.get_video("<insert_url_here>")
|
|
90
|
+
|
|
91
|
+
# Information from Video objects
|
|
92
|
+
print(video_object.title)
|
|
93
|
+
print(video_object.likes)
|
|
94
|
+
# Download the video
|
|
95
|
+
|
|
96
|
+
await video_object.download(downloader="threaded", quality="best", path="your_output_path + filename")
|
|
62
97
|
|
|
63
98
|
# SEE DOCUMENTATION FOR MORE
|
|
64
99
|
```
|
|
@@ -82,4 +117,4 @@ Pull requests are also welcome.
|
|
|
82
117
|
|
|
83
118
|
# License
|
|
84
119
|
Licensed under the LGPLv3 License
|
|
85
|
-
<br>Copyright (C) 2023–
|
|
120
|
+
<br>Copyright (C) 2023–2026 Johannes Habel
|
|
@@ -10,10 +10,45 @@
|
|
|
10
10
|
XHamster API is an API for xhamster.com. It allows you to fetch information from videos using regexes and requests.
|
|
11
11
|
|
|
12
12
|
# Disclaimer
|
|
13
|
-
> [!IMPORTANT]
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
13
|
+
> [!IMPORTANT]
|
|
14
|
+
> This is an unofficial and unaffiliated project. Please read the full disclaimer before use:
|
|
15
|
+
> **[DISCLAIMER.md](https://github.com/EchterAlsFake/API_Docs/blob/master/Disclaimer.md)**
|
|
16
|
+
>
|
|
17
|
+
> By using this project you agree to comply with the target site’s rules, copyright/licensing requirements,
|
|
18
|
+
> and applicable laws. Do not use it to bypass access controls or scrape at disruptive rates.
|
|
19
|
+
|
|
20
|
+
# Features
|
|
21
|
+
- Fetch videos + metadata
|
|
22
|
+
- Download videos
|
|
23
|
+
- Fetch Channels
|
|
24
|
+
- Fetch Pornstars
|
|
25
|
+
- Fetch Creators
|
|
26
|
+
- Fetch Shorts
|
|
27
|
+
- Search for videos
|
|
28
|
+
- Fetch playlists
|
|
29
|
+
- Asynchronous
|
|
30
|
+
- Built-in caching
|
|
31
|
+
- Easy interface
|
|
32
|
+
- Great type hinting
|
|
33
|
+
|
|
34
|
+
#### Networking Features
|
|
35
|
+
- HTTP 2.0 / HTTP 3.0
|
|
36
|
+
- Browser impersonation
|
|
37
|
+
- Custom JA3
|
|
38
|
+
- All proxy types
|
|
39
|
+
- Proxy authentication
|
|
40
|
+
- Speed Limit
|
|
41
|
+
- DNS over HTTPS
|
|
42
|
+
- And even more...
|
|
43
|
+
- All of this is configurable and can be adjusted as you like!
|
|
44
|
+
|
|
45
|
+
# Supported Platforms
|
|
46
|
+
This API has been tested and confirmed working on:
|
|
47
|
+
|
|
48
|
+
- Windows 11 (x64)
|
|
49
|
+
- macOS Sequoia (x86_64)
|
|
50
|
+
- Linux (Arch) (x86_64)
|
|
51
|
+
- Android 16 (aarch64)
|
|
17
52
|
|
|
18
53
|
# Quickstart
|
|
19
54
|
|
|
@@ -24,17 +59,19 @@ XHamster API is an API for xhamster.com. It allows you to fetch information from
|
|
|
24
59
|
```python
|
|
25
60
|
from xhamster_api import Client
|
|
26
61
|
# Initialize a Client object
|
|
27
|
-
client = Client()
|
|
28
62
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
video_object.
|
|
63
|
+
async def main():
|
|
64
|
+
client = Client()
|
|
65
|
+
|
|
66
|
+
# Fetch a video
|
|
67
|
+
video_object = await client.get_video("<insert_url_here>")
|
|
68
|
+
|
|
69
|
+
# Information from Video objects
|
|
70
|
+
print(video_object.title)
|
|
71
|
+
print(video_object.likes)
|
|
72
|
+
# Download the video
|
|
73
|
+
|
|
74
|
+
await video_object.download(downloader="threaded", quality="best", path="your_output_path + filename")
|
|
38
75
|
|
|
39
76
|
# SEE DOCUMENTATION FOR MORE
|
|
40
77
|
```
|
|
@@ -58,4 +95,4 @@ Pull requests are also welcome.
|
|
|
58
95
|
|
|
59
96
|
# License
|
|
60
97
|
Licensed under the LGPLv3 License
|
|
61
|
-
<br>Copyright (C) 2023–
|
|
98
|
+
<br>Copyright (C) 2023–2026 Johannes Habel
|
|
@@ -4,10 +4,10 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "xhamster_api"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2"
|
|
8
8
|
description = "A Python API for the Porn Site xhamster.com"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
11
|
license = "LGPL-3.0-only"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [
|
|
@@ -28,7 +28,7 @@ classifiers = [
|
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
av = ["av; python_version >= '3.10'"]
|
|
31
|
-
full = ["lxml"
|
|
31
|
+
full = ["lxml"] # H2 for HTTP 2.0 support, LXML for faster parsing speed, though optional :)
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
34
34
|
Homepage = "https://github.com/EchterAlsFake/xhamster_api"
|
|
@@ -7,21 +7,21 @@ import threading
|
|
|
7
7
|
|
|
8
8
|
from functools import cached_property
|
|
9
9
|
from urllib.parse import urlencode, quote
|
|
10
|
-
from base_api.modules.config import RuntimeConfig
|
|
11
|
-
from base_api.modules.errors import NetworkingError, BotProtectionDetected, UnknownError, InvalidProxy
|
|
12
|
-
from typing import Literal, AsyncGenerator, Any, Dict, List
|
|
13
|
-
from base_api.base import BaseCore, setup_logger, Helper
|
|
14
10
|
from curl_cffi import AsyncSession, Response
|
|
11
|
+
from base_api.modules.config import RuntimeConfig
|
|
12
|
+
from typing import Literal, AsyncGenerator, Any, Dict
|
|
15
13
|
from base_api.modules.type_hints import DownloadReport
|
|
14
|
+
from base_api.base import BaseCore, setup_logger, Helper
|
|
15
|
+
from base_api.modules.errors import NetworkingError, BotProtectionDetected, UnknownError, InvalidProxy, ResourceGone
|
|
16
16
|
|
|
17
17
|
try:
|
|
18
18
|
from modules.consts import *
|
|
19
19
|
from modules.errors import *
|
|
20
|
-
from modules.type_hints import
|
|
20
|
+
from modules.type_hints import *
|
|
21
21
|
except (ModuleNotFoundError, ImportError):
|
|
22
22
|
from .modules.consts import *
|
|
23
23
|
from .modules.errors import *
|
|
24
|
-
from .modules.type_hints import
|
|
24
|
+
from .modules.type_hints import *
|
|
25
25
|
|
|
26
26
|
try:
|
|
27
27
|
import lxml
|
|
@@ -30,7 +30,15 @@ except (ModuleNotFoundError, ImportError):
|
|
|
30
30
|
parser = "html.parser"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
async def
|
|
33
|
+
async def on_error(url: str, error: Exception, attempt: int) -> bool:
|
|
34
|
+
print(f"URL: {url}, ERROR: {error}, Attempt: {attempt}")
|
|
35
|
+
|
|
36
|
+
if isinstance(error, ResourceGone):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
async def get_html_content(core: BaseCore, url: str) -> str | None | dict:
|
|
34
42
|
# What should I do here?
|
|
35
43
|
try:
|
|
36
44
|
content = await core.fetch(url)
|
|
@@ -41,17 +49,17 @@ async def get_html_content(core: BaseCore, url: str) -> str | None:
|
|
|
41
49
|
if content.status_code == 404:
|
|
42
50
|
raise NotFound(f"Server returned 404 for: {url}")
|
|
43
51
|
|
|
44
|
-
except NetworkingError:
|
|
45
|
-
raise NetworkError from
|
|
52
|
+
except NetworkingError as e:
|
|
53
|
+
raise NetworkError(str(e)) from e
|
|
46
54
|
|
|
47
|
-
except InvalidProxy:
|
|
48
|
-
raise ProxyError from
|
|
55
|
+
except InvalidProxy as e:
|
|
56
|
+
raise ProxyError(str(e)) from e
|
|
49
57
|
|
|
50
|
-
except BotProtectionDetected:
|
|
51
|
-
raise BotDetection from
|
|
58
|
+
except BotProtectionDetected as e:
|
|
59
|
+
raise BotDetection(str(e)) from e
|
|
52
60
|
|
|
53
|
-
except UnknownError:
|
|
54
|
-
raise UnknownNetworkError from
|
|
61
|
+
except UnknownError as e:
|
|
62
|
+
raise UnknownNetworkError(str(e)) from e
|
|
55
63
|
|
|
56
64
|
|
|
57
65
|
class Something(Helper):
|
|
@@ -116,14 +124,18 @@ class Something(Helper):
|
|
|
116
124
|
def avatar_url(self) -> str:
|
|
117
125
|
return REGEX_AVATAR.search(self.html_content).group(1)
|
|
118
126
|
|
|
119
|
-
async def videos(self, pages: int = 2, videos_concurrency: int | None = None, pages_concurrency: int | None = None
|
|
127
|
+
async def videos(self, pages: int = 2, videos_concurrency: int | None = None, pages_concurrency: int | None = None,
|
|
128
|
+
on_video_error: on_error_hint = on_error,
|
|
129
|
+
on_page_error: on_error_hint = None
|
|
130
|
+
) -> AsyncGenerator[Video, None]:
|
|
120
131
|
page_urls = [build_page_url(url=self.url, is_search=False, idx=page) for page in range(1, pages + 1)]
|
|
121
132
|
videos_concurrency = videos_concurrency or self.core.configuration.videos_concurrency
|
|
122
133
|
pages_concurrency = pages_concurrency or self.core.configuration.pages_concurrency
|
|
123
134
|
assert videos_concurrency and pages_concurrency
|
|
124
135
|
|
|
125
136
|
async for video in self.iterator(use_alternative_constructor=True, video_link_extractor=extractor_shorts, target_page_urls=page_urls,
|
|
126
|
-
max_video_concurrency=videos_concurrency, max_page_concurrency=pages_concurrency
|
|
137
|
+
max_video_concurrency=videos_concurrency, max_page_concurrency=pages_concurrency,
|
|
138
|
+
on_video_error=on_video_error, on_page_error=on_page_error):
|
|
127
139
|
yield await video.init()
|
|
128
140
|
|
|
129
141
|
@cached_property
|
|
@@ -148,14 +160,18 @@ class Something(Helper):
|
|
|
148
160
|
|
|
149
161
|
return dictionary
|
|
150
162
|
|
|
151
|
-
async def get_shorts(self, pages: int = 2, videos_concurrency: int = 2, pages_concurrency: int = 1
|
|
163
|
+
async def get_shorts(self, pages: int = 2, videos_concurrency: int = 2, pages_concurrency: int = 1,
|
|
164
|
+
on_video_error: on_error_hint = on_error,
|
|
165
|
+
on_page_error: on_error_hint = None
|
|
166
|
+
) -> AsyncGenerator[Short, None]:
|
|
152
167
|
if not self.url.endswith("/"):
|
|
153
168
|
self.url += "/"
|
|
154
169
|
|
|
155
170
|
self.url += "shorts"
|
|
156
171
|
page_urls = [build_page_url(self.url, is_search=False, idx=page) for page in range(1, pages + 1)]
|
|
157
172
|
async for short in self.iterator(use_alternative_constructor=True, video_link_extractor=extractor_shorts, target_page_urls=page_urls,
|
|
158
|
-
max_video_concurrency=videos_concurrency, max_page_concurrency=pages_concurrency
|
|
173
|
+
max_video_concurrency=videos_concurrency, max_page_concurrency=pages_concurrency,
|
|
174
|
+
on_video_error=on_video_error, on_page_error=on_page_error):
|
|
159
175
|
yield await short.init()
|
|
160
176
|
|
|
161
177
|
class Channel(Something):
|
|
@@ -424,7 +440,10 @@ class Client(Helper):
|
|
|
424
440
|
date: Literal["latest", "weekly", "monthly", "yearly"] | None = None,
|
|
425
441
|
production: Literal["studios", "creators"] | None = None,
|
|
426
442
|
fps: Literal["30", "60"] | None = None,
|
|
427
|
-
pages: int = 2, videos_concurrency: int | None = None, pages_concurrency: int | None = None,
|
|
443
|
+
pages: int = 2, videos_concurrency: int | None = None, pages_concurrency: int | None = None,
|
|
444
|
+
on_video_error: on_error_hint = on_error,
|
|
445
|
+
on_page_error: on_error_hint = None
|
|
446
|
+
) -> AsyncGenerator[Video, None]:
|
|
428
447
|
path = quote(str(query), safe="") # e.g. "4k cats & dogs" -> "4k%20cats%20%26%20dogs"
|
|
429
448
|
base = f"https://xhamster.com/search/"
|
|
430
449
|
url = base + path
|
|
@@ -468,5 +487,6 @@ class Client(Helper):
|
|
|
468
487
|
assert isinstance(pages_concurrency, int)
|
|
469
488
|
|
|
470
489
|
async for video in self.iterator(use_alternative_constructor=True, video_link_extractor=extractor_shorts, target_page_urls=page_urls,
|
|
471
|
-
max_video_concurrency=videos_concurrency, max_page_concurrency=pages_concurrency
|
|
490
|
+
max_video_concurrency=videos_concurrency, max_page_concurrency=pages_concurrency,
|
|
491
|
+
on_video_error=on_video_error, on_page_error=on_page_error):
|
|
472
492
|
yield await video.init()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|